Skip to content

Commit ff36e10

Browse files
authored
Desktop (#2)
* init desktop * desktop fixed * bigger book cells * resize option * better Readme * version up
1 parent 7a5882a commit ff36e10

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+6271
-33
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@
2424

2525
# Debug mode (verbose logging)
2626
DEBUG=false
27+
28+
# Optional override for persistent app data directory (SQLite cache.db lives here)
29+
# MYBOOKSHELFAI_DATA_DIR=/absolute/path/to/mybookshelfai-data

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,5 @@ node_modules/
208208
data/
209209
*.db
210210
backups/
211+
/frontend/src-tauri/binaries/*
212+
!/frontend/src-tauri/binaries/.gitkeep

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ FRONTEND_PORT ?= 5173
55
PYTHON ?= python3
66
FRONTEND_DIR := $(CURDIR)/frontend
77

8-
.PHONY: help dev website backend frontend stop
8+
.PHONY: help dev website backend frontend stop desktop-dev desktop-build
99

1010
# Install JS deps when missing or after package-lock.json changes.
1111
$(FRONTEND_DIR)/node_modules/.bin/vite: $(FRONTEND_DIR)/package-lock.json
@@ -18,6 +18,8 @@ help:
1818
@echo " make backend – API only"
1919
@echo " make frontend – Vite only"
2020
@echo " make stop – kill anything listening on those ports"
21+
@echo " make desktop-dev – Linux desktop app dev mode (Tauri)"
22+
@echo " make desktop-build – Linux desktop build (Tauri)"
2123
@echo "Override Python: make PYTHON=.venv/bin/python dev"
2224

2325
# Run both servers in one shell so INT/TERM kills all background jobs and frees ports.
@@ -36,6 +38,12 @@ backend:
3638
frontend: $(FRONTEND_DIR)/node_modules/.bin/vite
3739
cd "$(FRONTEND_DIR)" && npm run dev
3840

41+
desktop-dev: $(FRONTEND_DIR)/node_modules/.bin/vite
42+
cd "$(FRONTEND_DIR)" && npm run desktop:dev
43+
44+
desktop-build: $(FRONTEND_DIR)/node_modules/.bin/vite
45+
cd "$(FRONTEND_DIR)" && npm run desktop:build
46+
3947
stop:
4048
@for p in $(BACKEND_PORT) $(FRONTEND_PORT); do \
4149
if command -v fuser >/dev/null 2>&1; then \

README.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,25 @@
33
[![CI](https://github.com/GoKitiky/MyBookshelfAI/actions/workflows/ci.yml/badge.svg)](https://github.com/GoKitiky/MyBookshelfAI/actions/workflows/ci.yml)
44

55
**AI-powered personal book recommender. Bring your own API key.**
6+
**Stop guessing your next read. Use your own notes to get recommendations that actually fit your taste.**
67

7-
Import your reading notes as Markdown files, get AI-powered enrichment (genres, themes, mood, complexity), a reader profile built from your library, and personalized book recommendations.
8+
MyBookshelfAI turns scattered Markdown book notes into a personal reading brain: clean library, AI enrichment, reader profile, and ranked recommendations.
89

9-
**Why this project:** keep notes and taste in one place, run everything locally (or in Docker), and plug in any OpenAI-compatible API—no vendor lock-in for how you host the app.
10+
## The pain it solves
1011

11-
**Limits:** you need your own LLM API key for enrichment and recommendations; data stays on your machine (SQLite). This is not a hosted SaaS.
12+
- **"I have notes everywhere, but no system."**
13+
Import your `.md` files and get one searchable library.
14+
- **"Most recommendation apps feel random."**
15+
Suggestions are based on *your* reading profile, not generic trends. AI extracts genres, themes, mood, and complexity from each note.
16+
- **"I do not want platform lock-in."**
17+
Bring your own API key, keep data local in SQLite, run locally or in Docker.
18+
19+
## Download Linux Desktop (fast)
20+
21+
- **Latest Linux builds:** [Open Downloads Page](https://github.com/GoKitiky/MyBookshelfAI/releases/latest)
22+
- **Formats planned for quick install:** AppImage and `.deb`
23+
24+
**Limits:** you need your own LLM API key for enrichment and recommendations. Data stays on your machine (SQLite). This is not a hosted SaaS.
1225

1326
## Screenshots
1427

@@ -92,6 +105,37 @@ Then open <http://localhost:5173>:
92105
2. **Import** your `.md` book notes.
93106
3. **Enrich****Build profile****Get recommendations**.
94107

108+
### Linux desktop (Tauri)
109+
110+
This repo also supports a Linux desktop build where the backend is started
111+
automatically as a Tauri sidecar (no separate `uvicorn` command).
112+
113+
Prerequisites:
114+
115+
- Rust toolchain (`rustup`, `cargo`)
116+
- Linux Tauri system dependencies (WebKitGTK, GTK3, libayatana-appindicator)
117+
- Ubuntu/Debian example: `sudo apt install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf`
118+
- PyInstaller for the backend sidecar build (`pip install pyinstaller`)
119+
120+
Desktop dev/build commands:
121+
122+
```bash
123+
cd frontend
124+
npm ci
125+
npm run desktop:dev
126+
# or: npm run desktop:build
127+
```
128+
129+
Equivalent Make targets from repo root:
130+
131+
```bash
132+
make desktop-dev
133+
# or: make desktop-build
134+
```
135+
136+
Desktop runtime data is stored in the app data directory resolved by Tauri.
137+
You can override it with `MYBOOKSHELFAI_DATA_DIR` when needed.
138+
95139
## Markdown File Format
96140

97141
Each book is one `.md` file. Title and author are extracted from the filename:

app/main.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import logging
44
from contextlib import asynccontextmanager
5-
from pathlib import Path
65
from typing import AsyncIterator
76

87
from fastapi import Depends, FastAPI, HTTPException
@@ -18,6 +17,7 @@
1817
from app.services.cache import init_cache
1918
from app.services.demo_seed import clear_demo_books
2019
from app.services.library_db import init_books_table, init_reading_lists_table
20+
from app.services.runtime_paths import ensure_data_dir
2121
from app.services.settings_db import init_settings_table, seed_from_env
2222

2323
logging.basicConfig(
@@ -28,7 +28,7 @@
2828

2929
@asynccontextmanager
3030
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
31-
Path("data").mkdir(exist_ok=True)
31+
ensure_data_dir()
3232
init_cache()
3333
init_books_table()
3434
init_reading_lists_table()
@@ -46,7 +46,10 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
4646

4747
app.add_middleware(
4848
CORSMiddleware,
49-
allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
49+
allow_origin_regex=(
50+
r"^(https?://(localhost|127\.0\.0\.1)(:\d+)?|"
51+
r"(tauri|app)://localhost|https?://tauri\.localhost)$"
52+
),
5053
allow_credentials=True,
5154
allow_methods=["*"],
5255
allow_headers=["*"],

app/services/cache.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
import sqlite3
66
from datetime import datetime, timedelta
77
from enum import Enum
8-
from pathlib import Path
98
from typing import Any
109

11-
DB_PATH = Path("data/cache.db")
10+
from app.services.runtime_paths import get_cache_db_path
11+
12+
DB_PATH = get_cache_db_path()
1213

1314
# Default TTLs per namespace (hours)
1415
_DEFAULT_TTL: dict[str, int] = {

app/services/library_db.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
import sqlite3
88
from collections.abc import Sequence
99
from datetime import datetime, timezone
10-
from pathlib import Path
1110
from typing import Any, Literal
1211

1312
from app.models import Book
13+
from app.services.runtime_paths import get_cache_db_path
1414

1515
ReadingListKind = Literal["planned", "blacklist"]
1616

17-
DB_PATH = Path("data/cache.db")
17+
DB_PATH = get_cache_db_path()
1818

1919

2020
class BookIdentityConflictError(Exception):

app/services/runtime_paths.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Runtime path helpers for local, container, and desktop environments."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
from pathlib import Path
7+
8+
DATA_DIR_ENV = "MYBOOKSHELFAI_DATA_DIR"
9+
DEFAULT_DATA_DIR = Path("data")
10+
11+
12+
def get_data_dir() -> Path:
13+
"""Return the directory used for persistent app data."""
14+
raw_override = os.environ.get(DATA_DIR_ENV, "").strip()
15+
if not raw_override:
16+
return DEFAULT_DATA_DIR
17+
return Path(raw_override).expanduser()
18+
19+
20+
def ensure_data_dir() -> Path:
21+
"""Create and return the persistent app data directory."""
22+
data_dir = get_data_dir()
23+
data_dir.mkdir(parents=True, exist_ok=True)
24+
return data_dir
25+
26+
27+
def get_cache_db_path() -> Path:
28+
"""Return the absolute/relative path to the SQLite cache database file."""
29+
return get_data_dir() / "cache.db"

app/services/settings_db.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
import sqlite3
66
from datetime import datetime, timezone
7-
from pathlib import Path
87

9-
DB_PATH = Path("data/cache.db")
8+
from app.services.runtime_paths import get_cache_db_path
9+
10+
DB_PATH = get_cache_db_path()
1011

1112
SETTING_LLM_API_KEY = "llm_api_key"
1213
SETTING_LLM_BASE_URL = "llm_base_url"

desktop/backend_launcher.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Desktop backend launcher entrypoint for the Tauri sidecar binary."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import os
7+
import signal
8+
import sys
9+
from pathlib import Path
10+
11+
import uvicorn
12+
13+
ROOT_DIR = Path(__file__).resolve().parent.parent
14+
if str(ROOT_DIR) not in sys.path:
15+
sys.path.insert(0, str(ROOT_DIR))
16+
17+
from app.main import app
18+
from app.services.runtime_paths import DATA_DIR_ENV
19+
20+
DEFAULT_HOST = "127.0.0.1"
21+
DEFAULT_PORT = 8315
22+
DEFAULT_LOG_LEVEL = "info"
23+
24+
25+
def parse_args() -> argparse.Namespace:
26+
"""Parse sidecar launch parameters."""
27+
parser = argparse.ArgumentParser(description="MyBookshelfAI desktop backend")
28+
parser.add_argument("--host", default=DEFAULT_HOST)
29+
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
30+
parser.add_argument("--log-level", default=DEFAULT_LOG_LEVEL)
31+
parser.add_argument("--data-dir", type=Path, default=None)
32+
return parser.parse_args()
33+
34+
35+
def main() -> None:
36+
args = parse_args()
37+
if args.data_dir is not None:
38+
os.environ[DATA_DIR_ENV] = str(args.data_dir.expanduser())
39+
40+
config = uvicorn.Config(
41+
app,
42+
host=args.host,
43+
port=args.port,
44+
log_level=args.log_level,
45+
reload=False,
46+
)
47+
server = uvicorn.Server(config=config)
48+
49+
def _handle_shutdown(_signum: int, _frame: object | None) -> None:
50+
server.should_exit = True
51+
52+
signal.signal(signal.SIGINT, _handle_shutdown)
53+
signal.signal(signal.SIGTERM, _handle_shutdown)
54+
server.run()
55+
56+
57+
if __name__ == "__main__":
58+
main()

0 commit comments

Comments
 (0)