Ctrl K
ComunidadGuías…

CONTRIBUTING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Contributing to Magnetio

Thanks for being here. Magnetio is a self-hosted Stremio addon, and the most
useful contributions are usually small and self-contained: a new torrent
provider, a new debrid service, a bug fix in title parsing.

This guide is written so that **adding a provider is your first contribution**,
not your fifth. You do not need to understand the whole codebase.

- [Ground rules](#ground-rules)
- [Repository layout](#repository-layout)
- [Running it locally](#running-it-locally)
- [Adding a torrent provider](#adding-a-torrent-provider)
- [Adding a debrid service](#adding-a-debrid-service)
- [Tests](#tests)
- [Style](#style)
- [Opening the pull request](#opening-the-pull-request)

---

## Ground rules

1. **No credentials in the repo.** Magnetio's whole premise is that the user's
   API keys never leave the user's server. Never commit a key, a token, or a
   real debrid account. `gitleaks` runs in CI and will fail your PR.
2. **No em-dash in prose, code, or commit messages.** Use commas, periods,
   parentheses, colons, or a regular hyphen. This is a house style rule and it
   is enforced in review.
3. **Every outbound request goes through `lib/httpClient.js`.** It carries the
   rate limiter, the retry policy, and the browser-like headers. Do not call
   `axios` or `fetch` directly from a provider.
4. **Use the shared helpers in `lib/magnetHelper.js`** for infohash extraction
   and size parsing, rather than reimplementing them in a provider. A
   hand-rolled infohash regex is a correctness bug, not a style preference:
   see [Return the right record shape](#3-return-the-right-record-shape).
5. **A provider must never throw.** Wrap the body in `try/catch`, log the
   failure with `logger.warn`, and return `[]`. One broken site must not take
   down a scrape.
6. **Apache-2.0.** By opening a PR you agree your contribution ships under the
   repository's license.

---

## Repository layout

```
addon/                 The Stremio addon: manifest, streams, sorting, subtitles
  moch/                Debrid services (Real-Debrid, Premiumize, TorBox, ...)
  lib/                 Config, filtering, sorting, subtitle pipeline
scraper/               The in-house torrent scraper back end
  providers/           One file per torrent site. This is where you start.
  lib/                 HTTP client, rate limiting, title parsing, SSRF guard
  test/                node:test suites
site/                  The landing and configure pages
deploy/                systemd units and monitoring for a self-hosted box
```

The addon and the scraper are two separate Node services that talk over HTTP.
A provider change only touches `scraper/`.

---

## Running it locally

You need Node 20 or newer.

```bash
git clone https://github.com/peterdsp/Magnetio.git
cd Magnetio

# Scraper
cd scraper
cp .env.example .env
npm install
npm start           # listens on :8080

# Addon, in a second terminal
cd ../addon
cp .env.example .env
npm install
npm start           # listens on :7000
```

Sanity checks against the running scraper:

```bash
curl localhost:8080/health
curl localhost:8080/providers
curl "localhost:8080/streams/movie/tt0068646"            # The Godfather
curl "localhost:8080/streams/series/tt0944947:2:3"       # Game of Thrones S02E03
curl "localhost:8080/streams/movie/tt0068646?providers=bt4g"   # one provider only
```

That last form is how you test your own provider in isolation.

---

## Adding a torrent provider

This is the best first contribution. A provider is one file, one export, and no
knowledge of the rest of the system.

### 1. Copy the template

```bash
cd scraper/providers
cp _template.js mysite.js
```

### 2. Implement the contract

A provider module exports exactly four things:

| Export | Type | Notes |
|---|---|---|
| `id` | `string` | lowercase, no spaces. Users pass this in `?providers=` |
| `name` | `string` | human-readable, used in logs and in the `provider` field |
| `types` | `string[]` | which of `movie`, `series`, `anime` it serves |
| `scrape(meta)` | `async function` | returns `Promise<TorrentRecord[]>` |

Declare `types` to match what `scrape()` actually does. A test checks that a
provider returns nothing for a type it does not claim, and
[PROVIDERS.md](PROVIDERS.md) is generated from these declarations.

`meta` is what the scraper knows about the requested content:

```js
{
  name: 'The Godfather',   // title to search for
  year: 1972,              // may be null
  imdbId: 'tt0068646',
  type: 'movie',           // 'movie' | 'series' | 'anime'
  season: 2,               // series only, may be undefined
  episode: 3,              // series only, may be undefined
}
```

Build your query string with `buildSearchQuery(meta)` from
`lib/titleHelper.js` rather than concatenating by hand. It produces
`"Show Name S02E03"` for series and `"Movie Name 1972"` for movies.

### 3. Run it without the server

```bash
cd scraper
npm run try -- mysite --name "The Shawshank Redemption" --imdb tt0111161
npm run try -- mysite --type series --name "Breaking Bad" --season 1 --episode 2
npm run try -- --list
```

This drives your `scrape()` directly, so you see real results from the site
with no server, client or Stremio install. It prints the hash, size and seeders
of each record, and warns when a result has no `infoHash` or a hash that is not
40-char lowercase hex, which is the most common mistake in a new provider.

If nothing comes back, the query shape is a likelier cause than your parser.
Some providers search by IMDb id rather than title, so pass `--imdb`, and anime
sites index absolute episode numbers rather than `S01E02`.

### 4. Return the right record shape

```js
{
  infoHash: 'a1b2...',     // REQUIRED, 40-char hex, lowercase
  title:    'The Godfather 1972 1080p BluRay x264',  // REQUIRED, raw release name
  seeders:  142,           // 0 if the site does not expose it
  leechers: 7,             // 0 if the site does not expose it
  size:     8589934592,    // bytes, 0 if unknown
  provider: 'MySite',      // same string as your `name` export
  imdbId:   meta.imdbId,   // pass it through
  ...parseTitle(title),    // adds quality, codec, source, languages, hdr, bitdepth
}
```

Two rules that matter more than they look:

- **`infoHash` must be 40-char lowercase hex, and you must get it from
  `extractInfoHash` in `lib/magnetHelper.js`.** Do not write your own regex.
  A `btih` value arrives in two encodings, 40-char hex and 32-char base32, and
  the shared helper converts base32 to hex. Lowercasing a base32 hash instead
  leaves a 32-char string that is *not* the torrent's infoHash: it will never
  deduplicate against the same torrent found on another site, and a debrid
  service will reject it. The bug stays invisible until the site serves its
  first base32 magnet. Use `parseSize` from the same module for the same
  reason.
- **Do not pre-filter or pre-sort.** Return everything the site gave you.
  `providers/index.js` deduplicates, `addon/lib/filter.js` filters, and
  `addon/lib/sort.js` sorts. A provider that tries to be clever fights the
  pipeline.

### 5. Register it

In `scraper/providers/index.js`, add the import alongside the others and add
the module to `ALL_PROVIDERS`:

```js
import * as mysite from './mysite.js';

const ALL_PROVIDERS = [
  // ...
  mysite,
];
```

Then register the same id on the add-on side, or it will not be selectable:
`TorrentProvider` in `addon/lib/types.js`, plus the next `sN` entry in
`PublicProviderAliases` in `addon/lib/configuration.js`. Without those, an
explicit `?providers=` list drops your provider and the presets built from
`Object.values(TorrentProvider)` skip it, so it only ever runs when a request
sends no provider filter at all. Regenerate the matrix with
`npm run doc:providers`.

### 6. Verify

```bash
cd scraper
npm test                                              # contract test picks it up automatically
curl "localhost:8080/streams/movie/tt0068646?providers=mysite"
curl "localhost:8080/streams/series/tt0944947:2:3?providers=mysite"
```

The contract test in `test/provider-contract.test.js` checks every registered
provider's shape without hitting the network. It will tell you if your `id` is
not unique, if `scrape` is missing, or if the module is not wired into the
aggregator.

### Which sites are worth adding

Prefer, in this order:

1. **Sites with an RSS or JSON search endpoint.** They are stable, cheap, and do
   not break on a CSS change. `bt4g.js` is the model.
2. **Sites behind a Torznab-compatible indexer.** Often you do not need a new
   provider at all: `torznab.js` already covers anything Jackett or Prowlarr can
   reach. Check that first.
3. **HTML scraping with `cheerio`.** Works, but expect to maintain it.

Avoid sites that require an account to search, that sit behind a Cloudflare
managed challenge on every path, or that have no magnet links.

### Reading an existing provider first

| File | Why it is a good reference |
|---|---|
| `subsplease.js` | Simplest correct case: RSS feed, shared magnet helper |
| `bt4g.js` | RSS feed with the size packed into the description |
| `yts.js` | Clean JSON API with real metadata |
| `nyaa.js` | Anime, non-standard season and episode naming |
| `torznab.js` | Generic indexer protocol, ID-based matching |
| `rutracker.js` | Non-Latin titles, `iconv-lite` for legacy encodings |

---

## Adding a debrid service

Slightly more involved than a provider, but still self-contained. Debrid
modules live in `addon/moch/` and are registered in two places:

1. `addon/moch/options.js`, which declares the config key and display name.
2. The `MOCH_MODULES` map in `addon/moch/moch.js`.

A debrid module exports:

| Export | Purpose |
|---|---|
| `getCachedStreams(streams, apiKey)` | bulk cache check, returns a map keyed by infoHash |
| `resolve({ apiKey, infoHash, fileIndex, ... })` | turn one infoHash into a direct link |

Not every service has a bulk cache-check endpoint. If yours does not, return
`null` from `getCachedStreams` and the addon falls back to on-demand entries
automatically (see `ON_DEMAND_LIMIT` in `moch.js`). `easydebrid.js` is the
shortest complete example; `realdebrid.js` is the most thorough.

**You do not need a paid account to open the PR.** Say so in the description
and a maintainer will verify against a live key before merging.

---

## Tests

```bash
cd scraper && npm test
cd addon   && npm test
```

Tests use the built-in `node:test` runner. No framework, no config.

```js
import test from 'node:test';
import assert from 'node:assert/strict';

test('describes the behaviour, not the function name', () => {
  assert.deepEqual(actual, expected);
});
```

What needs a test:

- Any change to title parsing, filtering, sorting, or season and episode
  matching. These are pure functions and regressions here are silent and
  user-visible. See `test/content-filter.test.js`.
- A parsing helper inside your provider (a size parser, an infohash extractor).
  Export it and test it directly.

What does not need a test: the network call itself. We do not mock the sites.
The contract test covers the module shape.

---

## Style

- ES modules, `async`/`await`, two-space indent. Match the file you are editing.
- Comments explain **why**, not what. The existing codebase is dense with
  comments that record a decision or a site quirk, which is the useful kind.
  Follow that.
- `logger.info` for a normal scrape result, `logger.warn` for a provider that
  failed. Never `console.log`.
- Prefix log lines with the provider in brackets: `logger.warn(\`[MySite] ${err.message}\`)`.
- No em-dash. See [Ground rules](#ground-rules).

---

## Opening the pull request

1. Branch off `main`: `git checkout -b provider/mysite`
2. Keep the diff minimal. One provider, or one fix, per PR. Unrelated
   refactors will be asked to come out.
3. Commit messages: `feat(scraper): add MySite provider`,
   `fix(scraper): correct BT4G size parsing`. Conventional prefixes, present
   tense.
4. In the description, say what you actually verified. Paste the output of your
   `?providers=mysite` call. A PR that shows real results from a real query
   gets merged quickly.

CI runs tests and a secret scan. Both must pass.

### Reviews

Maintained by one person in a European timezone, so expect a first response
within a few days rather than a few hours. If a PR goes quiet for a week, a
comment is welcome and not rude.

---

## Where to start

Issues labelled [`good first issue`](https://github.com/peterdsp/Magnetio/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
are each scoped to a single file, and name the file to copy and the interface to
implement. Comment on one to claim it so two people do not do the same work.

Found something not on the list? Open an issue first for anything larger than a
bug fix, so we can agree the shape before you write it.