Backend API for TorrentExplorer.
It stores .torrent files (either locally or in any S3-compatible bucket) together with MediaInfo metadata, and exposes them through a small REST API.
bun install
cp config.example.json config.json
# edit config.json
bun run startBy default, the server listens on http://0.0.0.0:3000.
All configuration lives in config.json.
At startup, environment variables can override values from the config file:
HOSTPORTPROXYTOKENXMRDATABASE_URLRELEASE_GROUPSTORAGE_DRIVER
Example:
Host interface to bind to.
Default:
"0.0.0.0"Port to listen on.
Default:
3000Bearer token required for authenticated API endpoints such as uploads.
Clients must send it as:
Authorization: Bearer <your_token>Controls how the server extracts the real client IP address.
This is important for IP-based rate limiting. If the server is behind a reverse proxy or CDN, you must configure this correctly so rate limiting is applied to the actual client IP instead of the proxy IP.
Supported presets:
directcloudflareawsgcpazurevercelnginxdevelopment
Use:
directwhen the server is exposed directly to the internet and not behind a proxycloudflarewhen traffic passes through Cloudflareawswhen deployed behind AWS proxy or load balancer infrastructuregcpwhen deployed behind Google Cloud infrastructureazurewhen deployed behind Azure infrastructurevercelwhen deployed on or behind Vercelnginxwhen using Nginx as a reverse proxydevelopmentfor local development setups where forwarded headers may be inconsistent
Example:
{
"server": {
"proxy": "cloudflare",
},
}If this value is set incorrectly, rate limiting may group all requests under the proxy IP instead of the real client IP.
donation.xmr Optional Monero donation address exposed by the API for frontend display.
{
"donation": {
"xmr": "8BmrgB8NGWhe8TSjNJDNMKgHrvxEQP1ZUDTWMNWA8CnKMpQjBjZhje1DPMmkbdNyMZESZDvHgMyufe5KPtLgy41Q8MTWnBE"
}
}database.url uses Bun's built-in SQL driver, so switching databases only requires changing the connection URL:
| Database | URL |
|---|---|
| SQLite | sqlite://data/torrents.db |
| PostgreSQL | postgres://user:pass@host:5432/db |
| MySQL | mysql://user:pass@host:3306/db |
The schema is migrated automatically on startup.
Supported values:
locals3
Torrent files are stored on disk using their original filenames.
Example:
{
"storage": {
"driver": "local",
"local": {
"path": "./torrents",
},
},
}Any S3-compatible provider can be used, including:
- AWS S3
- Cloudflare R2
- Backblaze B2
- MinIO
Example:
{
"storage": {
"driver": "s3",
"s3": {
"endpoint": "https://s3.example.com",
"region": "auto",
"bucket": "torrents",
"accessKeyId": "...",
"secretAccessKey": "...",
},
},
}Upload endpoints require bearer token authentication.
Send the configured token in the Authorization header:
Authorization: Bearer <your_token>Read-only endpoints do not require authentication unless you add your own external access control.
Returns basic server branding and release counts by category.
Example response:
{
"releaseGroup": "RabbitCompany",
"stats": { "anime": 42, "movies": 7, "series": 3 }
}Lists releases for a category.
This endpoint returns summary rows only and does not include the full MediaInfo text.
Example response:
{
"items": [
{
"id": 1,
"category": "anime",
"title": "Tsugumomo",
"year": 2017,
"season": "S02",
"torrent_name": "[RabbitCompany] Tsugumomo (2017) - S02 [Bluray-1080p][Opus 2.0][AV1]",
"tags": ["Bluray-1080p", "Opus 2.0", "AV1"],
"uploaded_at": 1713571200000
}
],
"pagination": { "page": 1, "limit": 24, "total": 42, "pages": 2 }
}Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page |
number | no | Page number (default: implementation-defined) |
limit |
number | no | Items per page (default: implementation-defined) |
q |
string | no | Search query |
Returns the full release record for a single item, including the raw MediaInfo text.
The frontend is expected to parse and render the MediaInfo content itself.
Creates a new release by uploading a torrent file and its corresponding MediaInfo.
This endpoint requires bearer token authentication.
Include the token in the Authorization header:
Authorization: Bearer <your_token>Send the request as multipart/form-data.
| Field | Type | Required | Notes |
|---|---|---|---|
torrent |
file | yes | A .torrent file. The original filename is preserved. |
mediainfo |
file or text | yes | MediaInfo text for the release. For batch uploads, use episode 1. |
The uploaded torrent filename must follow one of these formats:
-
Anime / Series
[ReleaseGroup] Title (Year) - S## [Tag1][Tag2]… -
Movies
[ReleaseGroup] Title (Year) [Tag1][Tag2]…
The API parses the following metadata from the filename:
- release group
- title
- year
- season (for anime/series)
- tags
Example:
curl -X POST http://localhost:3000/api/anime \
-H "Authorization: Bearer <your_token>" \
-F "torrent=@[RabbitCompany] Tsugumomo (2017) - S02 [Bluray-1080p][Opus 2.0][AV1].torrent" \
-F "mediainfo=@mediainfo.txt"Response:
201 Createdon success, with the newly created release in the response body
Streams the original .torrent file back to the client using its original filename.
This is intended for direct browser download or opening in a torrent client.
Build a single-file executable:
bun run buildRun it:
./torrent-explorer-serverA multi-stage Dockerfile is included.
Build the image:
docker build -t torrent-explorer-server .Run the container:
docker run -d \
--name torrent-explorer-server \
-p 3000:3000 \
-e PROXY=direct \
-e TOKEN=replace-with-a-long-random-token \
-e RELEASE_GROUP=RabbitCompany \
-v $(pwd)/torrents:/app/torrents \
-v $(pwd)/data:/app/data \
torrent-explorer-serverIf the container is behind a reverse proxy or CDN, set PROXY to the matching preset such as cloudflare or nginx so client IPs are extracted correctly for rate limiting.
A docker-compose.yml example is also included.
Start the service:
docker compose up -dExample Compose configuration:
services:
torrent-explorer:
image: rabbitcompany/torrent-explorer:latest
container_name: torrent-explorer
restart: unless-stopped
ports:
- "3000:3000"
environment:
- TZ=UTC
- PROXY=direct
- TOKEN=replace-with-a-long-random-token
- RELEASE_GROUP=RabbitCompany
- XMR=8BmrgB8NGWhe8TSjNJDNMKgHrvxEQP1ZUDTWMNWA8CnKMpQjBjZhje1DPMmkbdNyMZESZDvHgMyufe5KPtLgy41Q8MTWnBE
volumes:
#- ./config.json:/app/config.json
- torrent_explorer_torrents:/app/torrents
- torrent_explorer_data:/app/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
torrent_explorer_torrents:
driver: local
torrent_explorer_data:
driver: localIf you uncomment the config.json bind mount, values from that file are still overridden by supported environment variables.
When deploying behind Cloudflare, Nginx, or another proxy layer, change PROXY from direct to the correct preset. Otherwise all traffic may appear to come from the proxy, which breaks per-IP rate limiting.
{ "server": { "host": "0.0.0.0", "port": 3000, "proxy": "direct", "token": "hux23to2isshfuyttzlyy6dfn2m9vtfdpew6iyjUbRqxKtXhgx", }, "brand": { "releaseGroup": "RabbitCompany", }, "donation": { "xmr": "8BmrgB8NGWhe8TSjNJDNMKgHrvxEQP1ZUDTWMNWA8CnKMpQjBjZhje1DPMmkbdNyMZESZDvHgMyufe5KPtLgy41Q8MTWnBE", }, "database": { "url": "sqlite://data/torrents.db", }, "storage": { "driver": "local", "local": { "path": "./torrents", }, "s3": { "endpoint": "https://s3.example.com", "region": "auto", "bucket": "torrents", "accessKeyId": "...", "secretAccessKey": "...", }, }, }