Advanced BitTorrent-FUSE for real streaming to your private Plex/Jellyfin library. No Debrid.
10K+
████████╗ ██╗ ██████╗ █████╗ ███╗ ███╗ ██╗ ███████╗ ██╗ ██╗
╚══██╔══╝ ██║ ██╔══██╗ ██╔══██╗ ████╗ ████║ ██║ ██╔════╝ ██║ ██║
██║ ██║ ██████╔╝ ███████║ ██╔████╔██║ ██║ ███████╗ ██║ ██║
██║ ██║ ██╔══██╗ ██╔══██║ ██║╚██╔╝██║ ██║ ╚════██║ ██║ ██║
██║ ██║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ██║ ███████║ ╚██████╔╝
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝
Note
This project used to be called GoStream. It is now Tiramisu, same project, same codebase, just a new name. Nothing about how it works has changed, and everything in this README still applies. If you are upgrading from an older install, note that the binary name, config paths, and systemd service name have all changed too, so a fresh run of the install script is the easiest way to pick up the new layout.

Tiramisu exposes a custom FUSE virtual filesystem where every .mkv file is a perfect illusion: it looks like a real file on disk, but every byte is served live from a BitTorrent swarm on demand. No downloading. No temp files. No storage quota.
The BitTorrent engine runs inside the same OS process as the FUSE layer, connected by an in-memory io.Pipe(). When Plex/Jellyfin reads a byte range, there is no HTTP round-trip, no serialization, no proxy overhead, just bytes, flowing directly from peers through RAM to the media server at full speed.
The result: 4K HDR Dolby Vision, fully seekable, starting in 0.1 seconds, even on a Raspberry Pi 4.
This is not a torrent client with a media server bolted on. The FUSE filesystem is the product, custom-built from scratch around the constraints of torrent streaming: non-sequential byte-range requests, multi-gigabyte files that must be seekable at any position, and a Plex/Jellyfin scanner that probes every file in a library of hundreds of titles on startup.
The virtual filesystem is the product. Every .mkv in the library is a live
torrent that the media server sees as an ordinary file: the correct size,
seekable, with no temporary copies and nothing downloaded to disk. Below it sits
GoStorm, a deeply patched fork of
TorrServer Matrix and
anacrolix/torrent, running in the
same OS process as the FUSE layer, so there is no HTTP proxy and no serialization
between the swarm and the player. Both upstreams carry streaming patches that do
not exist in the originals.
The library keeps itself up to date. A built-in sync engine, pure Go with no Python and no external scripts, asks TMDB what is new, trending and popular on a schedule, searches Prowlarr with Torrentio as fallback, and files the best release it finds. When a better version appears later, it replaces the old one automatically: a 1080p entry becomes 4K HDR without anyone asking. TV series follow the same path, fullpack-first, in a Plex-compatible folder layout. Add a title to your Plex cloud watchlist and it shows up within the hour.
Your music and audiobooks ride the same filesystem. music/ and
audiobooks/ sit next to movies/ and tv/ under the same mount, served by the
same FUSE layer and filed through the same Library API. An album is added by
exact path with a caller-supplied MusicBrainz or ASIN identity
(external_id + external_id_ns), which is stored in the SQLite projection
registry and written into the virtual file, so a release keeps a real identity
instead of a filename guess. Audio projections are immutable and read-only to
their clients: files answer 0444, directories 0555, and the mutations a
scanner never needs (write open, truncate, rename, mkdir, chmod) are refused
with EROFS, while a direct unlink gets EPERM. Playback is served live from
the swarm exactly like video, so Plex and Plexamp index and play a lossless
album that occupies no disk. FLAC for music; M4B, M4A and MP3 for audiobooks.
Playback quality is the reason for the rest. These pieces exist to turn an unpredictable swarm into something a media player can trust:
Everything else is built in, not bolted on. The Control Panel at
:9080/control adjusts every FUSE and engine setting live, and the Health
Monitor Dashboard at :9080/dashboard shows a real-time speed graph, the
active stream with poster and quality badges, sync controls and system stats.
Plex and Jellyfin webhooks are handled natively: media.play switches the
stream to Priority Mode with aggressive piece prioritization, and the IMDB id is
extracted from the raw payload, so localized titles still match. NAT-PMP
lets a WireGuard setup request an inbound port mapping from the VPN gateway and
install the iptables REDIRECT rules, all without a restart. The peer
blocklist is optional and off by default; when enabled it is downloaded at
startup, refreshed every 24 hours, and injected into the engine before any
connection is made. A Library API (/api/library/add, /remove, /list)
lets any HTTP client file or remove a title, movies, series, music and
audiobooks alike, without touching the filesystem, and
hermes/SKILL.md is the ready-made skill that teaches an AI agent to use it.
All of it, engine included, ships as a single tiramisu binary.

Not a developer? This section explains what you actually get and why it works so well.

The end result: you open Infuse on your Apple TV, your entire movie library appears with posters and metadata, you press Play on a 4K Dolby Vision film and it starts in under a second. No buffering. No "downloading...". No subscription to Real-Debrid or any external service. Everything runs on a single board computer or any always-on Linux box in your home.
Tiramisu runs on your Linux device, a Raspberry Pi, a NAS, a VPS, or any always-on machine. It creates a virtual hard drive that looks completely real to the rest of your network: it contains thousands of .mkv files, each the correct size, each seekable. In reality, none of those files exist on disk. When anything reads a byte, Tiramisu silently fetches it in real-time from the BitTorrent network and passes it through.
Plex (or Jellyfin, or any media server) sees this virtual hard drive as a normal media library. It scans the files, downloads posters and descriptions from the internet, tracks what you've watched, and makes everything available on your home network, just like it would with a real NAS.
Infuse on Apple TV connects to your Plex/Jellyfin library and plays the files using Direct Play: it reads the video stream directly from the file, with no conversion or re-encoding. This is why it handles 4K HDR Dolby Vision effortlessly, even though it is coming from a torrent in real time.
Because Tiramisu exposes standard .mkv files on a standard filesystem, any player or media server that can read a network share works: Plex, Jellyfin, Emby, Kodi, VLC, mpv, or anything else. No plugins, no special configuration.
Tiramisu includes a built-in sync engine that runs on a schedule and keeps your library up to date without any manual intervention.
Every day, the engine queries TMDB (The Movie Database) for the latest releases, trending titles, and popular movies. For each title it finds, it searches Prowlarr (with Torrentio fallback) for the best available torrent (preferring 4K Dolby Vision, falling back to 1080p). If a good torrent is found, it registers it in GoStorm and creates the corresponding virtual .mkv file in the library.
The next time the media server scans, it finds a new file, downloads the poster and description, and the film appears in your library ready to play.
If a better version of a film becomes available later (for example a 4K HDR release of a title you already have in 1080p), the engine replaces it automatically.
TV series work the same way: the sync engine finds new seasons and episodes, organises them in the Plex/Jellyfin-compatible folder structure (Show Name (Year)/Season.01/), and they appear in your library within the week.
You can also add a title to your Plex Watchlist from any device and it will appear in your library within the hour.
To ensure the system remains functional even when public aggregators like Torrentio are down, Tiramisu includes a Prowlarr Adapter. This allows you to use your own self-hosted Prowlarr instance as the primary source for torrents.
The sync engine implements a Strict Fallback logic: it first queries your Prowlarr indexers using IMDB IDs for maximum precision. If no results are found locally, it automatically falls back to Torrentio.
Configuration is done via config.json (or the Control Panel → Prowlarr Indexer section):
"prowlarr": {
"enabled": true,
"api_key": "your-api-key",
"url": "http://192.168.1.x:9696"
}
See the Prowlarr Adapter Documentation for technical details.
Tiramisu has no external dependency at playback time. No third-party service, no monthly fee, no data leaving your home. Your library is always available, even without an internet connection, and it never disappears because a remote service went down.
When you press Play, Infuse immediately reads the beginning and end of the file to load the video index and seek tables. On a real hard drive this is instant. Tiramisu replicates this with an SSD warmup cache: the first 64 MB and last few MB of every file are pre-cached on the Pi's SSD during the initial Plex/Jellyfin library scan. By the time you press Play, those bytes are already on disk and Infuse gets them in milliseconds.
Every file on a real filesystem has a permanent ID called an inode. Plex/Jellyfin and Infuse use these IDs to recognize files across restarts, so they know "this is the same film I scanned last week" and do not re-download metadata or reset your watch history.
On a standard virtual filesystem, these IDs are random and change every time the system restarts. Tiramisu solves this by persisting a permanent inode map to a SQLite database (STATE/tiramisu.db). After a reboot, every virtual .mkv gets back the exact same ID it had before. To Plex/Jellyfin and Infuse, it is indistinguishable from a file that never moved.
You tap Play on Infuse, and Infuse asks Plex (or Jellyfin) for the file. Plex reads it from Tiramisu's virtual filesystem, which is where the illusion ends: the media server also fires a webhook at Tiramisu saying "user started playing this film". Tiramisu matches the event to a torrent and switches that stream to Priority Mode, so every spare byte of bandwidth goes to the film you are watching and background activity pauses. From there the bytes travel the only path they ever take: from BitTorrent peers into Tiramisu's memory, through the FUSE layer to Plex, from Plex to Infuse, and onto the TV. If you seek, the same machinery jumps straight to the new position in the torrent instead of re-buffering from the beginning.
Plex/Jellyfin reads /mnt/tiramisu-mkv-virtual/movies/Interstellar.mkv. From Plex/Jellyfin's perspective, it's a normal 55 GB file on a local disk. In reality, the file does not exist. The FUSE kernel module intercepts the read, calls into Tiramisu, and Tiramisu serves the exact bytes from a three-layer cache, backed by a live BitTorrent swarm.
| Layer | What | Size | Purpose |
|---|---|---|---|
| L1 | In-memory Read-Ahead | 256 MB | 32-shard concurrent buffer with per-shard LRU |
| L2 (optional) | SSD Warmup Head | 64 MB/file | Instant TTFF on repeat playback, served at 150–200 MB/s from SSD |
| L3 (optional) | SSD Warmup Tail | 16 MB/file | MKV Cues (seek index), Plex/Jellyfin probes the end of every file before confirming playback |
What makes this non-trivial: a FUSE filesystem that backs a real directory of static files is straightforward. A FUSE filesystem that must handle non-sequential byte-range requests across hundreds of files, each backed by an independent torrent with variable peer availability, while a Plex/Jellyfin scanner hammers every inode in parallel: that required building every subsystem from scratch.
BitTorrent Peers ←→ GoStorm Engine (:8090)
│
Native Bridge (In-Memory Pipe)
Zero-Network, Zero-Copy Hot Path
│
┌───────────────────────────────────┐
│ Tiramisu FUSE Layer │
│ L1: Read-Ahead Cache (256 MB) │
│ L2: SSD Warmup Head (64 MB/file) │
│ L3: SSD Warmup Tail (16 MB/file) │
└───────────────────────────────────┘
│
/mnt/tiramisu-mkv-virtual/*.mkv (FUSE mount)
│
Samba share (smbd, oplocks=no, vfs objects=fileid)
│
Synology CIFS mount (serverino, vers=3.0)
│
Plex/Jellyfin Media Server libraries
| Port | Purpose |
|---|---|
:8090 | GoStorm API, JSON torrent management |
:9080 | Control Panel, Metrics, Dashboard, Webhook, Scheduler, Library API |
Eleven purpose-built subsystems, each one the answer to a problem that surfaced while running this on Raspberry Pi 4 hardware.
A media server gives a streaming engine no second chances: it opens many files at once, reads the end of a file before the beginning, seeks wherever the user drags the bar, and expects the bytes to be there. On a Pi the margin is thin, so every subsystem below exists to remove one specific way the illusion can break.
Tiramisu is one process, not two. The GoStorm engine and the FUSE layer are
compiled into the same binary, so when the media server asks for a byte range
the FUSE side calls straight into the engine through an in-memory io.Pipe().
There is no TCP round trip, no HTTP headers to parse, no serialization and no
proxy in the middle; metadata lookups are plain Go function calls. That is what
removes the network latency that makes HTTP-based streaming proxies stutter on
small hardware.
An optional SSD cache that makes repeat playback almost instant. It is enabled and configured from the Control Panel → GoStorm settings, and it is independent of the core streaming path: without it Tiramisu still works, it just pays the cold-start cost again.
When enabled, the head cache keeps the first 64 MB of each file on SSD from the first play onward, so a repeat playback opens in under 0.01 s (an SSD reads at 150–200 MB/s, against 2–4 s for a cold torrent activation). The tail cache keeps the last 16 MB separately: MKV files store their Cues, the seek index, near the end, and the media server probes that region before confirming playback, which is why without it the seek bar can render as unavailable on the first open. The quota is configurable by LRU write time; the reference Raspberry Pi deployment uses 32 GB, enough for around 400 films. No manual warming is needed: a library scan reads the first 1 MB of every file, which is exactly what the head cache needs.
Tiramisu listens for the media server's "user pressed Play" event at
:9080/plex/webhook (the shorter /webhook works too), for Plex and
Jellyfin alike. When media.play arrives, four things happen at once: the
IMDB id is pulled out of the raw payload, the pieces covering the byte range
being played are given priority, the MKV Cues at the end of the file are frozen
so a seek does not have to fetch them again, and when playback stops the torrent is kept for 30 s, or for 5 s when it was only a scanner probe no webhook ever confirmed, so ordinary library traffic releases its peers almost at once.
The IMDB id is extracted with a regex before the payload is decoded, and that
is deliberate: Plex sends its identifiers in a non-standard Guid array that
makes a normal json.Unmarshal fail silently. The id is also what makes the
match language-independent, because media servers send titles in the user's
display language ("den stygge stesøsteren" instead of "The Ugly Stepsister")
and fuzzy matching fails on exactly those cases.
Plex: Settings → Webhooks → Add Webhook:
http://<your-pi-ip>:9080/plex/webhook
Jellyfin: install the
Webhook plugin, then add
one webhook with the events PlaybackStart, PlaybackStop and
PlaybackProgress:
| Field | Value |
|---|---|
| URL | http://<your-pi-ip>:9080/plex/webhook |
| Header Key | Content-Type |
| Header Value | application/json |
Template:
{"event":"{{NotificationType}}","Metadata":{"title":"{{{Name}}}","grandparentTitle":"{{{SeriesName}}}","librarySectionType":"{{ItemType}}","guid":"imdb://{{Provider_imdb}}","Guid":[{"id":"imdb://{{Provider_imdb}}"}]}}
Jellyfin's own event names and item types are mapped onto Tiramisu's internally
(PlaybackStart→media.play, PlaybackStop→media.stop, Movie→movie,
Episode→show), so neither side needs a code change or a plugin hack.
Music uses the same endpoint with a different identity: a track has no IMDb id,
so Tiramisu matches on the MusicBrainz id the projection was filed with. Plex and
Plexamp need no extra configuration: play an album and the log shows
[PLEX] Playback confirmed by webhook for: <file>.flac. Jellyfin needs a second
Generic destination (Jellyfin 10.11 requires plugin 18 or later, 10.10 requires 16
or later, current is 22) with the same URL and events and this body:
{"event":"{{NotificationType}}","Metadata":{"title":"{{{Name}}}","grandparentTitle":"{{{Artist}}}","librarySectionType":"{{ItemType}}","ProviderIds":{"MusicBrainzTrack":"{{Provider_musicbrainztrack}}","MusicBrainzReleaseGroup":"{{Provider_musicbrainzreleasegroup}}","MusicBrainzAlbum":"{{Provider_musicbrainzalbum}}","MusicBrainzArtist":"{{Provider_musicbrainzartist}}"}}}
Plex sends the release track id, Jellyfin the recording id (the MUSICBRAINZ_TRACKID
tag, exposed as MusicBrainzTrack). The id style follows the Plex/Jellyfin switch in
the Control Panel, so a library must be filed with the style of the player that
actually plays it, and switching player afterwards means re-filing it with an align
run in the new style. Jellyfin exposes MusicBrainz providers only when the files
carry MusicBrainz tags: tagless files still play, but their webhook has no id to
match and the session falls back to inferred playback.
Two read modes, managed automatically:
| Mode | Behavior | When |
|---|---|---|
| Responsive (default) | Data served before SHA1 verification, instant start | Normal operation |
| Strict | Only SHA1-verified pieces served | Automatically activated on corruption detection |
Responsive is the default because it is what makes playback start instantly:
bytes go out before their SHA1 is checked. When a corrupt piece is detected
(MarkNotComplete()), the shield switches to Strict and serves only verified
data, then waits for a clean streak before relaxing again. The streak starts at
30 seconds and grows by another 30 each time Strict re-triggers, up to a cap of
30 minutes; the first corruption in a session only logs and bans the peer,
leaving Fast mode on. The counter clears on a genuine clean streak, not on
seeks or a media.stop, so a swarm that keeps sending bad pieces gets
progressively more scrutiny instead of restarting the same short timer.
A peer that keeps corrupting is banned outright, and the ban is remembered for
30 days, across restarts. The mode switch itself is an atomic boolean, so the
hot read path pays no lock.
Seeking is the hardest thing a player can ask for: the target can be gigabytes away from where the pump is reading, and the player will not wait. Five coordinated fixes make it feel local:
| Fix | What it does |
|---|---|
| Eager offset update | Updates lastOff before the cache check, so the pump sees the target on the same Read() call |
| Atomic pipe interrupt | Interrupt() closes the pipe reader atomically when the player jumps > 256 MB, unblocking an io.ReadFull that is already waiting |
| Reactive jump | If the player is > 256 MB ahead of the pump, snap to (playerOff / chunkSize) * chunkSize |
| Pump survival | The pump survives ErrInterrupted with a 200 ms sleep-and-continue, with no goroutine restart |
| Tail probe detection | The media server's end-of-file MKV Cues probe is served from the SSD tail cache without steering the pump |
None of them works alone: the offset update tells the pump where the reader is going, the interrupt unblocks a read in flight, the reactive jump covers the huge gaps, the pump survives the interruption instead of being torn down
Content type
Image
Digest
sha256:2eb208afd…
Size
58.5 MB
Last updated
5 days ago
docker pull mrrobotogit/tiramisu