Files
mangaBookmark/README.md
T
claude f58d113934 feat: manga bookmark sync backend + Bromite userscript
Backend (Go, stdlib net/http + modernc.org/sqlite, CGO-free static binary):
- GET/PUT/DELETE /bookmarks{,/key} + /healthz
- bearer auth (constant-time), CORS origin reflection + 204 preflight
- SQLite store keyed <site>:<series_id>, last-write-wins, server-set updated_at
- httptest + temp-sqlite tests (auth, CORS, round-trip); go vet clean
- multi-stage Dockerfile (distroless static nonroot) + compose (base + prod proxy override)

Userscript (single Bromite-compatible IIFE, no GM_* APIs):
- Asura + Demonic adapters, URL-regex ids + og: title/cover
- Shadow-DOM floating UI, localStorage cache, optimistic sync
- auto-progress (no regress) + manual override; framework-agnostic nav watcher

Live-verified adapters (Playwright, 2026-07-24): asurascans.com /comics/<slug-hash>,
demonicscans.org /manga/<slug> + /title/<slug>/chapter/<n> — corrects the plan's
assumed /series/ paths and asuracomic.net domain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:48:28 +07:00

168 lines
6.4 KiB
Markdown

# Manga Bookmark
Track manga read-progress on **asurascans.com** (a.k.a. asuracomic.net) and
**demonicscans.org** from a phone (Bromite / mobile Chromium), synced to a
self-hosted Go backend so bookmarks unify across both sites and all devices.
Two parts:
- **`backend/`** — tiny Go (`net/http` + pure-Go SQLite) sync service. 4 routes,
static binary, distroless container.
- **`userscript/manga-bookmark.user.js`** — single Bromite-compatible userscript
(no `GM_*` APIs) that injects an on-page bookmark UI and syncs via `fetch()`.
```
Bromite userscript (isolated world, Shadow DOM UI, localStorage cache)
-- fetch() HTTPS --> reverse proxy (TLS + CORS) --> Go net/http --> SQLite (volume)
```
---
## 1. Backend
### Config (env)
| Var | Default | Notes |
|-----|---------|-------|
| `API_TOKEN` | *(required)* | Bearer token shared with the userscript. |
| `ALLOWED_ORIGINS` | Asura + Demonic origins | Comma-separated CORS allowlist. |
| `DB_PATH` | `/data/bookmarks.db` | SQLite file location. |
| `PORT` | `8080` | Plain HTTP; TLS terminated by the proxy. |
### Endpoints
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/bookmarks` | Bearer | All bookmarks (single-user). |
| `PUT` | `/bookmarks/{key}` | Bearer | Upsert one series; server sets `updated_at`. |
| `DELETE` | `/bookmarks/{key}` | Bearer | Remove one. |
| `GET` | `/healthz` | none | `200 ok`. |
`key` is `<site>:<series_id>` — e.g. `asura:trash-of-the-counts-family-f886a8af`
or `demonic:Infinite-Level-Up-in-Murim`. Sync is last-write-wins.
### Develop / test
```bash
cd backend
go test ./... # unit + handler tests
CGO_ENABLED=0 go build # static binary
```
### Run the stack
```bash
cp .env.example .env
# edit .env: set API_TOKEN (openssl rand -hex 32)
docker compose up -d --build # binds 127.0.0.1:8080
```
Smoke test:
```bash
TOKEN=$(grep '^API_TOKEN=' .env | cut -d= -f2)
curl -s localhost:8080/healthz # ok
curl -s localhost:8080/bookmarks # 401
curl -s -H "Authorization: Bearer $TOKEN" localhost:8080/bookmarks # []
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"Test","last_chapter":"Chapter 1","last_chapter_num":1}' \
localhost:8080/bookmarks/asura:test-1
curl -s -i -X OPTIONS -H 'Origin: https://asurascans.com' \
-H 'Access-Control-Request-Method: PUT' \
localhost:8080/bookmarks/asura:test-1 | grep -i access-control # 204 + CORS headers
```
### Deploy behind your reverse proxy
Route `https://manga-api.<domain>` → the service on `:8080` (TLS at the proxy).
- **Host proxy** (nginx/Caddy on the host): the base compose already binds
`127.0.0.1:8080`; point the proxy `proxy_pass http://127.0.0.1:8080;`.
- **Docker proxy** (Traefik/nginx in a container on its own network): use the
override, which drops the published port and joins the shared network:
```bash
docker network create proxy # once, if it doesn't exist
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
```
Set `PROXY_NETWORK` in `.env` if your network isn't named `proxy`.
Verify: `https://manga-api.<domain>/healthz` returns `ok` over valid TLS (no
mixed-content), and an `OPTIONS` preflight from a real site origin returns the
CORS headers.
---
## 2. Userscript
### Configure
Edit the config block at the top of `userscript/manga-bookmark.user.js`:
```js
const API_BASE = "https://manga-api.<domain>"; // no trailing slash
const API_TOKEN = "<same token as backend>";
```
The token lives in the userscript's **isolated world** — the manga sites' own
JS cannot read it.
### Install on Bromite (mobile)
Bromite runs Chromium's native userscript engine (no Tampermonkey needed):
1. Bromite → **Settings → User scripts** → enable user scripts (allow the
permission prompt).
2. Save the configured `manga-bookmark.user.js` to the device (or open its raw
URL). Bromite detects the `.user.js` and offers to install it.
3. Confirm the install; the `@match` list covers both sites.
4. Open a series on either site — a 📑 button appears bottom-right.
> Exact menu wording varies by Bromite build; if "User scripts" is absent,
> update Bromite or use a build with userscript support.
### Desktop iteration (optional)
The script is `GM_*`-free, so it also runs in Tampermonkey/Violentmonkey on
desktop for faster testing — install the same file unchanged.
### Use
- **Bookmark**: on a series or chapter page, open the panel → **+ Bookmark this**.
- **Auto-progress**: opening a chapter of a bookmarked series records it when the
chapter number ≥ the stored one (re-reading older chapters never regresses
progress; unparseable numbers set the current chapter).
- **Manual override**: panel → **Edit** on any row forces a specific chapter.
- **Continue**: jumps to the last-read chapter (or the series page).
- Bookmarks made on Asura appear when the panel is opened on Demonic, and vice
versa — the backend is the shared store.
Offline / backend down: changes are cached in `localStorage` and retried on the
next successful load (last-write-wins).
---
## Adapter reference (verified live 2026-07-24)
The site adapters key everything off URL regex, with `title`/`cover` from
`og:title` / `og:image`. Confirmed against live pages via Playwright:
| Site | Series URL | Chapter URL | `series_id` |
|------|-----------|-------------|-------------|
| **Asura** (`asurascans.com`; `asuracomic.net` 301s here) | `/comics/<slug-hash>` | `/comics/<slug-hash>/chapter/<n>` | `<slug-hash>` |
| **Demonic** (`demonicscans.org`) | `/manga/<slug>` | `/title/<slug>/chapter/<n>/<page>` (`chaptered.php?manga=<id>&chapter=<n>` 301s here) | `<slug>` |
Notes:
- Asura `og:title` carries a `Chapter N - Read Online \| Asura Scans` suffix that
the adapter strips; Demonic chapter `og:title` is `<Title> Chapter N`.
- Demonic's `<slug>` is identical on `/manga/…` and the canonical `/title/…`
reader, so a bookmark set from the series page and the auto-update from the
reader resolve to the **same key**.
- Asura showed no Next.js markers on the live site, so navigation uses a
framework-agnostic watcher (history patch + polling) rather than a Next-only
hook — works for client-routed and full-reload sites alike.
If either site changes its URL shape, update the regex in the matching adapter
in `userscript/manga-bookmark.user.js` and re-verify.