From 44c2888a2eda07e2d663cebb2395b37c641c4355 Mon Sep 17 00:00:00 2001 From: Michal Pemcak Date: Mon, 19 Oct 2026 18:41:22 +0200 Subject: [PATCH] docs: add OKF knowledge base --- CLAUDE.md | 1 + doc/index.md | 9 +++++ doc/log.md | 4 +++ doc/okf-base.yaml | 9 +++++ doc/structure/index.md | 5 +++ doc/structure/overview.md | 69 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+) create mode 100644 CLAUDE.md create mode 100644 doc/index.md create mode 100644 doc/log.md create mode 100644 doc/okf-base.yaml create mode 100644 doc/structure/index.md create mode 100644 doc/structure/overview.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c284537 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +Knowledge for this project is in doc/ (start at doc/index.md). diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..30b8d54 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,9 @@ +--- +okf_version: "0.1" +--- + +# webshare-api + +Webshare.cz HTTP API: a Torznab indexer, a fake qBittorrent client, a Web UI, and an MCP server for AI agents. It speaks the official [Webshare.cz](https://webshare.cz) HTTP API and re-exposes it as protocols *arr apps understand (Torznab search plus a qBittorrent-compatible download client), so Sonarr/Radarr can download Webshare files. Because Webshare download links expire after roughly ten minutes, this service resolves a fresh CDN link at fetch time instead of embedding one. + +- [Structure](./structure/) - What the project is, its stack, repo layout, and the verified build/test/run commands. diff --git a/doc/log.md b/doc/log.md new file mode 100644 index 0000000..0958ce1 --- /dev/null +++ b/doc/log.md @@ -0,0 +1,4 @@ +# Update Log + +## 2026-10-19 +- **Creation**: Bundle created; [project overview](/structure/overview.md) added. diff --git a/doc/okf-base.yaml b/doc/okf-base.yaml new file mode 100644 index 0000000..60077c9 --- /dev/null +++ b/doc/okf-base.yaml @@ -0,0 +1,9 @@ +# Manifest for okflint: `okflint validate --manifest okf-base.yaml` (core OKF rules only). +okf_version: "0.1" +base: + name: webshare-api + roots: + - path: "." + reserved_files: + index: index.md + log: log.md diff --git a/doc/structure/index.md b/doc/structure/index.md new file mode 100644 index 0000000..79fdcfb --- /dev/null +++ b/doc/structure/index.md @@ -0,0 +1,5 @@ +# Structure + +What the project is, how it is laid out, and how to build, test and run it. + +- [Project Overview](./overview.md) - purpose, stack, repo layout, verified build/test/run commands. diff --git a/doc/structure/overview.md b/doc/structure/overview.md new file mode 100644 index 0000000..50a8530 --- /dev/null +++ b/doc/structure/overview.md @@ -0,0 +1,69 @@ +--- +type: Project +title: webshare-api Project Overview +description: Purpose, stack, repo layout, and the verified build/test/run commands for the Webshare.cz HTTP API service. +tags: [project, node, express, torznab, qbittorrent, mcp] +timestamp: 2026-10-19T00:00:00Z +--- + +# webshare-api Project Overview + +## Purpose + +`webshare-api` re-exposes [Webshare.cz](https://webshare.cz) as the protocols *arr apps understand, plus an [MCP](https://modelcontextprotocol.io) server for AI agents. The roles: + +- **Torznab indexer** — `GET /api?t=caps|search|tvsearch|movie` turns Webshare search into Torznab RSS/XML, so Sonarr/Radarr can use it as an indexer. +- **Fake torrent** — `GET /download/:ident` returns a minimal `.torrent` whose webseed points at this service's `/stream/:ident`, never at a CDN URL (Webshare links expire in ~10 minutes). +- **Stream** — `GET /stream/:ident` fetches a fresh Webshare `file_link` and issues an HTTP 302 to the CDN. +- **Fake qBittorrent client** — `/api/v2/*` is a qBittorrent-compatible download API that streams the HTTPS file to disk and reports progress; this is what *arr apps hand the torrent to. +- **Web UI** — `GET /`, a two-tab TUI-style page (Search, Queue) sharing the same download engine as the fake qBittorrent client. +- **MCP server** — `POST /mcp` (Streamable HTTP, stateless POST-only) exposes `webshare_search`, `webshare_get_link`, and `webshare_login_check` to AI agents. + +Webshare credentials are **optional**: with empty `WEBSHARE_USERNAME`/`WEBSHARE_PASSWORD` the app runs in guest mode using unauthenticated `file_link` (no `wst`). Guest downloads work but use the free-tier CDN, which is typically much slower than an authenticated account. + +## Stack + +- **Runtime**: Node.js `>=18`, CommonJS modules. +- **Framework**: Express 4 (CORS wide open for LAN UI access; 4 MB JSON body limit). +- **Build**: [esbuild](https://esbuild.github.io) (`scripts/build.mjs`) bundles the server and the optional MCP entry each into a **single `.js` file** with dependencies inlined (no `node_modules` at runtime), and inlines the Web UI into one `dist/ui/index.html` (CSS + JS inlined). +- **Tests**: Jest 30 + supertest. `npm test` runs `jest --coverage --runInBand`. Coverage thresholds are enforced globally: **90% lines, 80% branches, 90% functions, 90% statements** over `src/**/*.js` (excluding `src/index.js`, which is the entrypoint that starts listening). Test timeout is 15 s and Jest is configured with `forceExit: true`. +- **Docker**: multi-stage build (`Dockerfile`); the final image copies only `dist/` (no source or `node_modules`), so the image is far smaller than `npm ci --omit=dev`. +- **Commit hooks**: [lefthook](https://github.com/evilmartians/lefthook) (`lefthook.yml`) runs gitleaks (secret scan), a large-file guard, and a conventional-commit subject check. **No repo-wide formatter is enforced.** + +## Repo layout + +``` +src/ # source (CommonJS) + index.js # express app + app.listen at module load (server entry) + qbt.js # fake qBittorrent client / download queue; cancelDownload returns a boolean + mcp.js # MCP server entry (Streamable HTTP) + webshare.js # Webshare API client + torznab.js # Torznab caps/feed builders + torrent.js # minimal .torrent generation +ui/ # Web UI source (index.html, styles.css, scripts.js) +scripts/build.mjs # esbuild: server.js + mcp.js + single-file UI +dist/ # build output (gitignored; produced by npm run build or Docker) +__tests__/ # Jest + supertest tests +compose.yaml # Docker Compose +data/ # host download data dir (Compose mount target) +``` + +The `src/` modules: `qbt.js` is the shared download engine used by both the Web UI queue and the fake qBittorrent API; `cancelDownload(hash)` returns a boolean indicating whether a matching download was cancelled. + +## Build / test / run (verified) + +| Command | What it does | +|---------|--------------| +| `npm install` | Install dependencies. | +| `npm run build` | `node scripts/build.mjs` → `dist/server.js` (~1.9 MB, all deps bundled), `dist/mcp.js` (~1.8 MB), `dist/ui/index.html` (CSS+JS inlined). | +| `npm start` | `node dist/server.js` — run the built server (also serves MCP on `PORT`). | +| `npm run start:dev` | `node src/index.js` — run unbundled `src/` for local hacking (no build step). | +| `npm run mcp` / `npm run mcp:dev` | Run the MCP-only process (no Torznab/qBittorrent), serving `POST /mcp`. | +| `npm test` | `jest --coverage --runInBand` (enforces the coverage thresholds above). | +| `docker compose up -d --build` | Run via Compose (see `compose.yaml`; copy `.env.example` to `.env` first). | + +Default listen port is `3001` (`PORT`). Webshare credentials are read from `WEBSHARE_USERNAME` / `WEBSHARE_PASSWORD`; full configuration reference lives in the root `README.md`. Do not commit a filled `.env`. + +## Related + +- Full configuration table, *arr integration steps, and MCP client config: root [`README.md`](../../README.md).