A plugin host for EverythingBox. It loads media-source plugins, exposes them over EverythingBox's addon protocol (manifest, catalog, detail, stream), and serves whatever those plugins hand back.
It ships the host. It does not ship any source.
EverythingBoxServer on its own has no built-in source of media. Everything a running
server can browse, search, or stream comes from a plugin you install — the
plugins/<key>/ folder is empty in a fresh checkout, and the server runs fine with
nothing in it; it just has nothing to serve.
Sources arrive as plugins. What you point one at is your decision and your responsibility.
A plugin is one assembly with a public, parameterless-constructible IPlugin:
public sealed class MyPlugin : IPlugin
{
public string Key => "myplugin"; // namespaces this plugin's ids and config section
public string DisplayName => "My Plugin";
public Version ApiVersion => new(ServerApi.VersionString);
public void Configure(IPluginRegistry registry, IPluginContext context)
{
registry.AddSource(new MySource(context));
}
}Configure registers one or more IMediaSources — each one owns its own catalogs, its
own search, and its own stream resolution:
public interface IMediaSource
{
string Key { get; }
IReadOnlyList<CatalogDescriptor> Catalogs { get; }
Task<SourceCatalog> SearchAsync(string catalogId, string? query, SourceContext ctx, CancellationToken ct);
Task<SourceCatalog> DetailAsync(string itemId, SourceContext ctx, CancellationToken ct);
Task<SourceStream?> ResolveAsync(string itemId, int index, SourceContext ctx, CancellationToken ct);
// Optional — implement only what you need.
Task<ProxyResponse?> OpenAsync(string itemId, string? rangeHeader, CancellationToken ct);
Task<WarmUpResult> WarmUpAsync(CancellationToken ct);
}IPluginContext gives a plugin a logger factory, a shared HttpClient the host owns
(do not dispose it), a plugin-private cache directory created before Configure runs,
and its own section of the server config via GetConfig<T>().
Drop the build output in plugins/<key>/ and restart. Each plugin loads into its own
AssemblyLoadContext so its dependencies cannot collide with another plugin's or the
host's, gets a private cache directory and its own config section, and is skipped with a
logged error — rather than taking the server down — if it fails to load, declares an
incompatible ApiVersion, or throws while registering.
One rule for every plugin project: reference EverythingBox.Server.Abstractions with
Private="false". The host already supplies that assembly at runtime; copying a second
copy into a plugin's own output folder loads it twice, and every cast between the host's
IMediaSource and the plugin's stops working because, to the runtime, they're now two
unrelated types with the same name.
See docs/ARCHITECTURE.md for the full contract and
EverythingBox.Server.SampleSource/ for a complete working plugin — it scans a
configured folder and serves the files it finds.
everythingbox-server.json next to the executable (override the path with EBS_CONFIG):
Any key not shown above is not read by anything — the config loader ignores unknown
properties rather than erroring, so a typo or a stale example silently does nothing.
Indexers ships empty and Debrid/DownloadClient ship unset — a fresh checkout
searches and streams nothing until you configure at least one indexer, the same way it
installs no plugin.
Every entry degrades rather than refuses to start: a blank/unparseable indexer
BaseUrl, a Debrid block with no ApiKey, or an unrecognized Debrid.Provider /
DownloadClient.Kind is logged and skipped (GrabberFactory.cs) — a typo in one line
does not stop the server from booting with everything else it understood.
Set AccessToken before exposing the server to the internet. It becomes a URL path
prefix (/<token>/manifest.json and friends), so anyone without it cannot reach your
server's routes at all.
Configuring at least one Indexers entry makes the server searchable without
installing any plugin. The host registers a built-in idx: source
(IndexerSearchSource) exposing one search-only catalog per media type the pipeline
understands — movies, series, music, audiobooks, books, comics — backed by every
configured indexer (plus any a plugin registers, see AddIndexer below), but only once
at least one indexer exists. With Indexers: [] and no plugin-registered indexer, the
manifest declares none of these catalogs at all, rather than advertising six shelves
that can never return anything.
Add Debrid to make a search result playable: resolving a release asks the configured
debrid service (TorBox or Real-Debrid today) for a direct link. If the release isn't
already cached on the debrid service, the server returns a "still caching, try again
shortly" notice — unless the self-download fallback below is switched on.
When debrid reports a release as still caching, the server can fetch it itself instead of
handing back the notice — ReleaseStreamResolver joins the swarm via
MonoTorrentDownloader (an ITorrentDownloader), saves the file into FilesCacheDir, and
serves it back as a hosted files/... URL the client plays directly.
This is off by default, and turning it on is a network-exposure decision, not a speed setting. Every other path here talks only to services you configured (an indexer, a debrid provider). This one joins a BitTorrent swarm from the host's own IP address to download the release, which is a different thing to opt into. It exists for the case where waiting on debrid to cache a release isn't acceptable and fetching it yourself is.
Download controls it:
Enabled—falseby default. Nothing is ever self-downloaded until you set thistrue.MaxSizeMB— releases larger than this are never fetched (default2048), so a fallback finishes while you're still interested. A release whose size the indexer didn't report is treated as over the cap and never fetched — the point of the cap is to refuse an unbounded download, and "we don't know how big it is" is exactly that.TimeoutSeconds— a stalled or seedless swarm is given up on after this long (default600) and the request falls back to the caching notice, rather than being held open indefinitely.
Concurrent viewers of the same still-caching release share one download rather than each starting their own; a timed-out or empty attempt is not memoized, so a later retry can still succeed.
DownloadClient (qBittorrent or Transmission) feeds the same pipeline but is not
exercised by anything the host calls today — no route hands a release to a download
client yet. Ranking (seeders, size bounds, preferred resolution/language/format,
banned terms) has no such gap: it's applied to both the single-best ranked path
(GrabAsync) and the search catalogs described above, via ITorrentGrabber.SearchRankedAsync
— search, dedupe, parse, then rank and filter, best first, ineligible releases
removed per Ranking.
Search is not the only way in. IPluginRegistry.AddMetadata registers an
IMetadataSource (EverythingBox.Server.Abstractions/Metadata/IMetadataSource.cs),
decoupled from any one indexer:
public interface IMetadataSource
{
string Name { get; }
IReadOnlyList<string> SupportedMediaTypes { get; } // protocol strings — "movie", "series"
Task<IReadOnlyList<MetadataItem>> BrowseAsync(string mediaType, string? query, CancellationToken ct);
// Optional — defaults to empty, so a movie-only source need not implement it.
Task<IReadOnlyList<MetadataEpisode>> EpisodesAsync(string seriesId, CancellationToken ct);
}MetadataItem carries an id, title, media type, and optionally a year and poster URL
— MediaType is the protocol string this item actually is ("movie", "series"), and
the host shelves it by that, not by whichever catalog it was returned from, so a
source mixing types into one BrowseAsync call gets each item shelved correctly (or
dropped, if it doesn't match). MetadataEpisode carries a season, episode, title, and
optionally an overview — no id of its own; nothing downstream needs one.
The host's built-in meta: source (Sources/MetadataBackedVideoSource.cs) pairs
every registered IMetadataSource with the same indexer/debrid pipeline idx: uses:
browsing meta:movies/meta:series lists titles from the metadata source(s); opening
a series (/detail/series/{id}.json) expands it into episodes via EpisodesAsync. A
bare series id has no single release to resolve — only a movie or one of its episodes
does; resolving either searches the pipeline (SearchRankedAsync, the same ranked path
described above) for a matching release, then hands it to the same Debrid-backed
resolution idx: uses. The metadata source only supplies what to browse; the
Indexers/Debrid config above is still what makes a browsed title actually playable.
Same "advertise only what can be filled" discipline as idx:: meta:movies is
declared only when a registered metadata source's SupportedMediaTypes includes
"movie" (independently for meta:series/"series"). With no metadata plugin
installed, neither catalog appears — the server ships no metadata source, the same
way it ships no indexer.
EverythingBox.Server.Core is a standalone, dependency-free library for the search →
parse → rank → resolve pipeline a torrent-backed source needs. The host
(EverythingBox.Server) wires it up from the Indexers/Debrid/DownloadClient/
Ranking config above via GrabberFactory and exposes it as the idx: search catalogs
described above — see "Search, out of the box". A plugin can also build directly on the
library itself, same as before this wiring existed:
var grabber = new GrabberBuilder()
.AddProvider(new TorznabProvider(httpClient, new TorznabOptions { BaseUrl = ... }))
.UseDownloadClient(new QBittorrentClient(httpClient, new QBittorrentOptions { BaseUrl = ... }))
.Build();
var result = await grabber.GrabAndDownloadAsync(new MovieRequest { Title = "..." });TorrentGrabber(GrabberBuilderfor fluent setup) queries every capableITorrentProviderin parallel, deduplicates results by info hash, runs them throughDefaultReleaseParser(resolution, source, codecs, season/episode, etc.), and picks the best viaDefaultTorrentRanker.GrabberOptionscontrols timeouts, dedup, parsing, and an optional "quick grab" score threshold that stops early.- Providers:
DirectProviderBaseis the template for a single-tracker HTTP provider (three methods: build query, build URI, parse response).TorznabProvideris the built-in adapter for any Torznab-speaking indexer manager — point it at a Prowlarr or Jackett endpoint viaTorznabOptions.BaseUrl(or theIndexersconfig key above) and it can search everything that manager aggregates. - Debrid:
IDebridServiceimplementationsRealDebridServiceandTorBoxService(underEverythingBox.Server.Core/Debrid/) turn a grabbed release into direct download links viaGrabAndResolveAsync.MagnetResolverand the bundled bencode parser (TorrentInfo) let a.torrent-only result be treated like a magnet one, with no BitTorrent library dependency. - Download clients:
IDownloadClientimplementationsQBittorrentClientandTransmissionClient(underEverythingBox.Server.Core/Download/) hand a grabbed release to a running qBittorrent or Transmission instance viaGrabAndDownloadAsync. - Selection and caching:
MediaFileMatchernarrows a multi-file release down to the one file a request asked for (an episode out of a season pack, a track out of an album).FileResolverCacheis a size-bounded on-diskIResolverCache.RemoteZipreads a single member out of a remote ZIP over HTTP range requests, including nested zips, without downloading the archive.RetryHandleris anHttpClientDelegatingHandlerwith exponential-backoff retry for transient failures. - Also included:
ConsoleCatalog, a factual table of retro game consoles (names, aliases, ROM extensions) useful to any emulator front end.
EverythingBox.Server.Core still takes no third-party package (see
CoreDependencyTests); anything the pipeline needs beyond the BCL — an archive
library, a BitTorrent library — belongs in the host or a plugin behind an interface
instead, which is exactly how the host itself (EverythingBox.Server.csproj, an ASP.NET
Core project) is allowed to take on the ASP.NET Core dependencies Core cannot.
Alongside AddSource, IPluginRegistry has a second, smaller registration tier:
void AddIndexer(ITorrentProvider provider);An indexer registered this way is merged with every config-defined Indexers entry
into the same shared pipeline — it inherits dedupe, parsing, ranking, and resolution for
free rather than implementing search end to end the way an IMediaSource does. Reach
the resulting pipeline (and the configured debrid service) from any plugin via
IPluginContext.Server:
public interface IServerServices
{
ITorrentGrabber Grabber { get; } // do not call from Configure — see the interface's own doc comment
IDebridService? Debrid { get; }
IFileCache Files { get; }
}A third, independent registration:
void AddMetadata(IMetadataSource metadata);A metadata source only supplies what to browse — the built-in MetadataBackedVideoSource
pairs it with the shared indexer/debrid pipeline automatically (see "Browse, with a
metadata plugin" above). A plugin registering one does not need to also register an
indexer or a source; the host already has a pipeline to search once Indexers/Debrid
are configured.
A fourth registration tunes the shared pipeline rather than adding to it:
void AddProviderTracker(IProviderPerformanceTracker tracker);An IProviderPerformanceTracker learns which indexers actually pay off. Before each
search the grabber calls Prioritize to order providers best-first (so a "quick grab"
score threshold can finish sooner), and after each search it calls Record with one
ProviderOutcome per provider (result count, whether it errored, how long it took,
whether it produced the winning release) — an implementation persists those stats so the
ordering improves across runs. At most one applies across the whole server: a single
plugin's registry rejects a second registration, and if two different plugins each
register one, the first in load order wins and the rest are logged and dropped
(Program.cs).
dotnet build
dotnet testTarget framework .NET 9. EverythingBox.Server.Abstractions — the only assembly a
plugin references — takes exactly one dependency, Microsoft.Extensions.Logging.Abstractions.
EverythingBox.Server.Core takes none — BCL only, enforced by a test. The host project
is an ASP.NET Core web app and carries the usual ASP.NET Core dependencies.
EverythingBoxServer ships no content and no content source. What a running server can find and serve depends entirely on the plugins you install and what you point them at. You are responsible for ensuring your use complies with the terms of any service you configure and the laws of your jurisdiction.
MIT licensed. See LICENSE.
The pieces below don't exist yet. They're the direction, not the current state — nothing in this section is installed, callable, or configurable today.
- A SharpCompress-backed archive reader and
ArchiveNormalizer, so an archive-packaged release (.zip/.rar/.7z) can be browsed and streamed member-by-member the way a single video/audio/document file already can.
This is host-side (EverythingBox.Server), not EverythingBox.Server.Core, because it
needs a package Core is not allowed to take — the same reason the now-shipped
MonoTorrent-backed self-download fallback (see Download above) lives there.
{ "Listen": "http://0.0.0.0:7000", "AccessToken": null, // required if reachable from the internet "PluginsDirectory": null, // defaults to "plugins" next to the executable "FilesCacheDir": null, // defaults to "files" next to the executable "Manifest": { "Id": "com.everythingbox.server", "Name": "EverythingBox Server", "Version": "1.0.0", "Description": "Media from the sources you configure.", "Accent": "#3E8E7E" }, "Plugins": { }, // one opaque section per installed plugin, keyed by plugin Key "Indexers": [ // Torznab endpoints (e.g. Prowlarr or Jackett). Ships EMPTY. { "Name": "My Prowlarr", "BaseUrl": "http://localhost:9696/1/api", "ApiKey": "..." } ], "Debrid": null, // { "Provider": "torbox" | "realdebrid", "ApiKey": "..." } "DownloadClient": null, // { "Kind": "qbittorrent" | "transmission", "BaseUrl": "...", "Username": "...", "Password": "...", "Category": "..." } "Download": { // self-download fallback for an uncached release — OFF by default "Enabled": false, // fetch it over BitTorrent from THIS host's own IP instead of waiting on debrid "MaxSizeMB": 2048, // never self-download a release larger than this — or one of unknown size "TimeoutSeconds": 600 // give up on a stalled/seedless swarm instead of holding the request open }, "Ranking": { } // MinSeeders, MinSizeBytes, MaxSizeBytes, PreferredResolutions, // PreferredAudioFormats, PreferredLanguages, PreferredSubtitleLanguages, // BannedTerms, RequireRelevanceMatch — see RankingOptions for defaults }