Sign inSign up

mrrobotogit/tiramisu

By mrrobotogit

•Updated 5 days ago

Advanced BitTorrent-FUSE for real streaming to your private Plex/Jellyfin library. No Debrid.

Image
0

10K+

mrrobotogit/tiramisu repository overview

████████╗ ██╗ ██████╗   █████╗  ███╗   ███╗ ██╗ ███████╗ ██╗   ██╗
╚══██╔══╝ ██║ ██╔══██╗ ██╔══██╗ ████╗ ████║ ██║ ██╔════╝ ██║   ██║
   ██║    ██║ ██████╔╝ ███████║ ██╔████╔██║ ██║ ███████╗ ██║   ██║
   ██║    ██║ ██╔══██╗ ██╔══██║ ██║╚██╔╝██║ ██║ ╚════██║ ██║   ██║
   ██║    ██║ ██║  ██║ ██║  ██║ ██║ ╚═╝ ██║ ██║ ███████║ ╚██████╔╝
   ╚═╝    ╚═╝ ╚═╝  ╚═╝ ╚═╝  ╚═╝ ╚═╝     ╚═╝ ╚═╝ ╚══════╝  ╚═════╝

⁠Tiramisu is the most advanced BitTorrent engine and FUSE virtual filesystem for live streaming to your private Plex or Jellyfin library. Hermes and OpenClaw ready. Forget Real-Debrid.

Ask DeepWiki Mentioned in Awesome Selfhosted License: GPL v3 Docker Pulls Docker Image Size

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: click play on a torrent, streaming starts in about a second

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.

⁠What's included

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:

  • Adaptive Shield trades a little speed for data integrity only when a peer is actually sending bad data, then relaxes once the swarm proves clean. A peer that keeps corrupting pieces is banned outright, and the ban is remembered for 30 days, across restarts.
  • TailHedge removes the stutter caused by one slow peer holding up the exact byte the player is waiting for, by asking a second peer for the same data and using whichever arrives first.
  • PEXChurn drops peers that turn out to be useless for the file you are actually watching, freeing connection slots for peers that can help.
  • Adaptive Chunk Size scales the buffer to what the file needs instead of pinning one fixed size for every bitrate.

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.


⁠Control Panel

Tiramisu Control Panel overview


⁠Table of Contents


⁠The Setup: Tiramisu + Plex/Jellyfin/Infuse on Apple TV

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

Plex library populated by sync scripts

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.

⁠How the three pieces fit together

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.

⁠How your library gets populated automatically

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.

⁠Prowlarr Integration (Resilience)

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.

⁠100% local, no subscriptions

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.

⁠Why Infuse starts in under a second

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.

⁠Why your library survives a reboot

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.

⁠When you press Play: the full chain

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.


⁠How the Magic Works

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.

LayerWhatSizePurpose
L1In-memory Read-Ahead256 MB32-shard concurrent buffer with per-shard LRU
L2 (optional)SSD Warmup Head64 MB/fileInstant TTFF on repeat playback, served at 150–200 MB/s from SSD
L3 (optional)SSD Warmup Tail16 MB/fileMKV 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.


⁠Architecture

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 Map
PortPurpose
:8090GoStorm API, JSON torrent management
:9080Control Panel, Metrics, Dashboard, Webhook, Scheduler, Library API

⁠Core Engineering

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.

⁠1. Zero-Network Native Bridge

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.

⁠2. Two-Layer SSD Warmup Cache (optional)

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.

⁠3. Webhook Integration & Smart Streaming

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:

FieldValue
URLhttp://<your-pi-ip>:9080/plex/webhook
Header KeyContent-Type
Header Valueapplication/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.

⁠4. Adaptive Shield

Two read modes, managed automatically:

ModeBehaviorWhen
Responsive (default)Data served before SHA1 verification, instant startNormal operation
StrictOnly SHA1-verified pieces servedAutomatically 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.

⁠5. Seek-Master Architecture

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:

FixWhat it does
Eager offset updateUpdates lastOff before the cache check, so the pump sees the target on the same Read() call
Atomic pipe interruptInterrupt() closes the pipe reader atomically when the player jumps > 256 MB, unblocking an io.ReadFull that is already waiting
Reactive jumpIf the player is > 256 MB ahead of the pump, snap to (playerOff / chunkSize) * chunkSize
Pump survivalThe pump survives ErrInterrupted with a 200 ms sleep-and-continue, with no goroutine restart
Tail probe detectionThe 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

Tag summary

Content type

Image

Digest

sha256:2eb208afd…

Size

58.5 MB

Last updated

5 days ago

docker pull mrrobotogit/tiramisu