Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ A short, hand-picked list of trusted sources:
| Category | Sources |
| --- | --- |
| Games | FitGirl |
| Movies | YTS, The Pirate Bay, 1337x, BitTorrented |
| TV | EZTV, The Pirate Bay, 1337x, BitTorrented |
| Movies | YTS, The Pirate Bay, BitTorrented |
| TV | EZTV, The Pirate Bay, BitTorrented |
| Anime | Nyaa, SubsPlease |

Games are the only category that can run code, so they come from FitGirl alone, a repacker with a long, trusted track record; everything else is plain video and subtitles. If a source is down, the search carries on without it, and torlink tells you which one is offline.
Expand All @@ -73,26 +73,24 @@ Everything else starts with a torrent someone else made. `seed` goes the other w

It turns the folder into a torrent, saves `album.torrent` next to it, prints the magnet, and starts sharing right away. Send anyone the magnet and they pull the files from you.

`serve` takes a `.torrent` as well as a magnet, so you can hand it one you already have:
### Driving it over HTTP

`serve` takes a magnet or a `.torrent` you already have:

POST /add {"magnet":"magnet:?xt=..."}
POST /add {"torrent":"<base64>"}

Either can carry a `seedTime` for that one torrent, in the same grammar as `--seed-time` (`"30d"`, `"2h"`; `0` means never stop). It wins over the daemon-wide flag, so a box that normally drops seeds after a couple of hours can keep one release alive for a month. Change it later, or on something already downloading, through the control endpoint:
Either can carry a `seedTime` for that one torrent, in the same grammar as `--seed-time` (`"30d"`, `"2h"`; `0` means never stop). It wins over the daemon-wide flag, so a box that normally drops seeds after a couple of hours can keep one release alive for a month. Change it later, or on something already downloading, through the control endpoint; `GET /downloads` reports the limit and when it falls due (`seedUntil`):

POST /control {"id":"<info hash>","action":"seed-time","seedTime":"30d"}

`GET /downloads` reports the limit and when it falls due (`seedUntil`) on every torrent that has one.

### Binding to a VPN or interface

If your torrents should only ever leave through one interface, name it:
If torrents should only ever leave through one interface, name it:

torlnk serve --interface wg0 --peer-port 51413

Every peer, tracker, DHT and search socket is then bound to that interface's IPv4 address (`wg0`, `tun0`, `NordLynx`, `utun4`, or the address itself), and `--peer-port` fixes the TCP/uTP listen port so one forward on the VPN side covers it. If the interface drops, downloads and seeds pause instead of falling back to another route, and they pick up on their own when it is back, on the new address if that changed. Nothing is tied to a particular VPN: torlink only knows the interface. LSD, UPnP, NAT-PMP and WebRTC peers stay off while bound, since each would use another interface; whoever owns the interface's gateway owns the port mapping. `--dht-port` fixes the DHT port too (it cannot share the peer port, where uTP already listens).

The same flags work on `watch` and `seed`, `search` takes `--interface` alone, and the TUI honors them as `torlnk --interface wg0` or through `TORLINK_INTERFACE`, `TORLINK_PEER_PORT` and `TORLINK_DHT_PORT` (handy for `attach`) without changing anything on screen: a stalled download looks stalled. Daemons should take the flag rather than the variable, since a restart after `torlnk update` keeps the flags and not the environment. `serve` reports the state on `GET /network` (address, whether it is up, the listen port, and how many peers dialed in, which is the proof a forwarded port works), and `POST /network {"interface":"wg0","peerPort":51413,"dhtPort":null}` changes any of them in place with no restart, so a script that reads a forwarded port from your VPN client can hand it over. Limits, stated plainly: IPv4 only, DNS lookups go through the OS resolver, and the `git`/`npm` processes `torlnk update` spawns are not bound.
Every peer, tracker, DHT and search socket is bound to that interface's IPv4 address, and the fixed peer port is the one to forward. If the interface drops, downloads and seeds pause instead of taking another route, and pick up on their own when it is back. The same flags work on `watch`, `seed` and the TUI, or as `TORLINK_INTERFACE`, `TORLINK_PEER_PORT` and `TORLINK_DHT_PORT`, with nothing new on screen. `serve` reports the state on `GET /network` and takes changes on `POST /network` without a restart. torlink knows nothing about any particular VPN, only the interface; `torlnk --help` has the details and limits.

## Contributing

Expand Down
2 changes: 2 additions & 0 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ network (TUI/watch/serve/seed; search takes --interface alone):
--dht-port <n> fixed DHT UDP port (must differ from --peer-port)
Env: TORLINK_INTERFACE, TORLINK_PEER_PORT, TORLINK_DHT_PORT (flags win; a
daemon restarted by torlnk update keeps its flags, not its environment).
IPv4 only; DNS lookups use the OS resolver, and the git/npm processes that
torlnk update spawns are not bound.
serve adds GET /network (address, online, listen port, incoming peers) and
POST /network {"interface":..,"peerPort":..,"dhtPort":..} to change them in
place, no restart; null clears a field.
Expand Down
2 changes: 1 addition & 1 deletion src/cli/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe("runSearch", () => {
expect(searchMock.mock.calls.map(([source]) => source.id)).toEqual(movieSourceIds);
expect(execution.exitCode).toBe(0);
expect(execution.document.sources.yts).toEqual({ ok: true, count: 1, error: null, code: null });
expect(execution.document.sources["x1337-movies"]).toEqual({
expect(execution.document.sources.bittorrented).toEqual({
ok: false,
count: 0,
error: "source unavailable",
Expand Down
4 changes: 2 additions & 2 deletions src/cli/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ export async function runSearch(options: {
]);
// A source cut short may still hand back what it already had (its
// deeper pages are best-effort), and that is worth keeping. An empty
// answer after the deadline is not a search with no matches, though:
// 1337x drops every row whose detail page it could not fetch.
// answer after the deadline is not a search with no matches, though: a
// source cut off mid-way may have dropped every row it could not finish.
if (results === CUT_OFF || (deadline.signal.aborted && results.length === 0)) {
return timedOut(source);
}
Expand Down
3 changes: 0 additions & 3 deletions src/sources/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@ import { fitgirl } from "./fitgirl";
import { nyaa } from "./nyaa";
import { subsplease } from "./subsplease";
import { tpbMovies, tpbTv } from "./piratebay";
import { x1337Movies, x1337Tv } from "./x1337";
import { yts } from "./yts";
import type { Source, SourceGroup, SourceId } from "./types";

export const SOURCES: readonly Source[] = [
fitgirl,
yts,
tpbMovies,
x1337Movies,
eztv,
tpbTv,
x1337Tv,
nyaa,
subsplease,
bittorrented,
Expand Down
2 changes: 0 additions & 2 deletions src/sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ export type SourceId =
| "subsplease"
| "tpb-movies"
| "tpb-tv"
| "x1337-movies"
| "x1337-tv"
| "bittorrented";

export type SourceGroup = "Games" | "Movies" | "TV" | "Anime";
Expand Down
23 changes: 0 additions & 23 deletions src/sources/x1337.test.ts

This file was deleted.

162 changes: 0 additions & 162 deletions src/sources/x1337.ts

This file was deleted.

12 changes: 6 additions & 6 deletions src/ui/dedupe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,22 @@ describe("dedupeResults", () => {
it("keeps the healthiest row's own fields", () => {
const out = dedupeResults([
row({ source: "tpb-movies", seeders: 3, name: "TPB name" }),
row({ source: "x1337-movies", seeders: 90, name: "1337x name" }),
row({ source: "eztv", seeders: 90, name: "EZTV name" }),
]);
expect(out).toHaveLength(1);
expect(out[0]!.seeders).toBe(90);
expect(out[0]!.source).toBe("x1337-movies");
expect(out[0]!.name).toBe("1337x name");
expect(out[0]!.source).toBe("eztv");
expect(out[0]!.name).toBe("EZTV name");
});

it("carries the losing row's trackers into the surviving magnet", () => {
// The row 1337x returns carries the torrent's own announce list; the row
// The row EZTV returns carries the torrent's own announce list; the row
// buildMagnet() writes carries only torlink's public defaults. Whichever
// wins on seeders, both lists have to survive.
const own = "udp://private.example:6969/announce/passkey";
const out = dedupeResults([
row({ source: "tpb-movies", seeders: 90 }),
row({ source: "x1337-movies", seeders: 3, magnet: buildMagnet(HASH, "x", [own]) }),
row({ source: "eztv", seeders: 3, magnet: buildMagnet(HASH, "x", [own]) }),
]);
expect(out).toHaveLength(1);
expect(out[0]!.seeders).toBe(90);
Expand All @@ -60,7 +60,7 @@ describe("dedupeResults", () => {
const out = dedupeResults([
row({ source: "tpb-movies", seeders: 1, magnet: buildMagnet(HASH, "x", [a]) }),
row({ source: "yts", seeders: 5, magnet: buildMagnet(HASH, "x", [b]) }),
row({ source: "x1337-movies", seeders: 2, magnet: buildMagnet(HASH, "x", [a]) }),
row({ source: "eztv", seeders: 2, magnet: buildMagnet(HASH, "x", [a]) }),
]);
expect(out).toHaveLength(1);
const tr = trackers(out[0]!.magnet);
Expand Down
4 changes: 2 additions & 2 deletions src/ui/dedupe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type { TorrentResult } from "../sources/types";
// one worth showing — but the rows it beats are folded into it instead of being
// dropped:
//
// - their announce lists. 1337x and EZTV hand back the torrent's own
// trackers, while rows built by buildMagnet() carry only torlink's public
// - their announce lists. EZTV hands back the torrent's own trackers,
// while rows built by buildMagnet() carry only torlink's public
// defaults, so keeping the higher-seeder row alone can trade a working
// announce list for a generic one. Same loss #146 fixed for a .torrent
// file's own trackers.
Expand Down
2 changes: 0 additions & 2 deletions src/ui/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ export const SOURCE_STYLE: Record<SourceId, { tag: string; color: string }> = {
subsplease: { tag: "SUB", color: "#b9a7e6" },
"tpb-movies": { tag: "TPB", color: "#5fd0c5" },
"tpb-tv": { tag: "TPB", color: "#5fd0c5" },
"x1337-movies": { tag: "1337", color: "#f6a55c" },
"x1337-tv": { tag: "1337", color: "#f6a55c" },
bittorrented: { tag: "BT", color: "#7db8f0" },
};

Expand Down
Loading