POST /api/queue/cancel answers 404 {"error":"hash not in queue"} for
untracked hashes instead of 200 ok:true. The UI surfaces the server's
error message instead of swallowing it.
71 lines
5.3 KiB
Markdown
71 lines
5.3 KiB
Markdown
---
|
|
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
|
|
ui-api.js # Web UI JSON API routes (search, queue add/list/cancel), mounted before static
|
|
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. The UI-facing JSON routes live in `ui-api.js` (extracted from `index.js` so they are testable with supertest); see [Web UI JSON API](./ui-api.md) for their contracts, including the `404` on cancelling a hash that is not in the queue. The fake qBittorrent API in `qbt.js` deliberately keeps its always-`Ok.` contract for real *arr clients.
|
|
|
|
## 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).
|