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.
5.3 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Project | webshare-api Project Overview | Purpose, stack, repo layout, and the verified build/test/run commands for the Webshare.cz HTTP API service. |
|
2026-10-19T00:00:00Z |
webshare-api Project Overview
Purpose
webshare-api re-exposes Webshare.cz as the protocols *arr apps understand, plus an MCP server for AI agents. The roles:
- Torznab indexer —
GET /api?t=caps|search|tvsearch|movieturns Webshare search into Torznab RSS/XML, so Sonarr/Radarr can use it as an indexer. - Fake torrent —
GET /download/:identreturns a minimal.torrentwhose webseed points at this service's/stream/:ident, never at a CDN URL (Webshare links expire in ~10 minutes). - Stream —
GET /stream/:identfetches a fresh Websharefile_linkand 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) exposeswebshare_search,webshare_get_link, andwebshare_login_checkto 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 (
scripts/build.mjs) bundles the server and the optional MCP entry each into a single.jsfile with dependencies inlined (nonode_modulesat runtime), and inlines the Web UI into onedist/ui/index.html(CSS + JS inlined). - Tests: Jest 30 + supertest.
npm testrunsjest --coverage --runInBand. Coverage thresholds are enforced globally: 90% lines, 80% branches, 90% functions, 90% statements oversrc/**/*.js(excludingsrc/index.js, which is the entrypoint that starts listening). Test timeout is 15 s and Jest is configured withforceExit: true. - Docker: multi-stage build (
Dockerfile); the final image copies onlydist/(no source ornode_modules), so the image is far smaller thannpm ci --omit=dev. - Commit hooks: 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 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.