Skip to content

Core Concepts

TheMinecraftGuyGuru edited this page Jun 8, 2026 · 4 revisions

Core Concepts

This page explains how MuxCore works — from the big ideas down to the details. If you're coming from Sonarr, Radarr, or Plex, start here. You don't need to be a programmer to understand this page. When we get into code examples, we'll mark them clearly.


The Problem MuxCore Solves

How Media Automation Works Today

Right now, most people run a stack of separate programs:

Sonarr (TV)  +  Radarr (Movies)  +  Lidarr (Music)  +  Bazarr (Subtitles)
     ↓               ↓                   ↓                  ↓
Prowlarr (search) → qBittorrent (download) → Plex/Jellyfin (playback)

Each program has its own:

  • Database — the same movie exists in three places
  • Settings — configure download paths six times
  • Scheduler — six separate task timers running
  • Update cycle — six things to keep current

This works fine for a single machine with a simple setup. It breaks down when you want to:

  • Run transcoding on a separate GPU machine
  • Store some media locally and some in cloud storage
  • Have two downloaders active at the same time
  • Add a new media type (books, audiobooks, comics)
  • Survive a machine going down without manual intervention

How MuxCore Does It

MuxCore replaces the separate programs with a single platform where everything is a pluggable module:

                         MuxCore (the platform)
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
   Downloaders           Media Managers         Playback
   (qBit, SABnzbd)    (Movies, TV, Music)   (Jellyfin, Plex)
        │                     │                     │
        └─────────────────────┼─────────────────────┘
                              │
                    Everything else:
              Indexers, Transcoders, Notifications,
              Metadata, Subtitles, Storage backends...

Instead of six programs that barely know about each other, you have one platform where every module can see what every other module is doing. A download finishes → the media manager imports it → the transcoder optimizes it → notification goes out. All without hardcoded paths or fragile integrations.


The Fabric Metaphor

MuxCore uses a consistent metaphor to explain how it works. Once you understand this, everything else clicks into place.

MuxCore is a fabric — a woven system of independent parts working together.

Concept What it means Real-world analogy
The Loom Core platform — the fixed frame The frame of a weaving loom that never changes shape
Threads Modules — every feature is a thread Individual threads woven into the fabric
Patterns Contracts — the interfaces threads agree to The weaving pattern that determines how threads cross
Signals Events — how threads communicate A pulse that travels through the fabric when something changes
Tapestries Workflows — multi-step processes A finished woven piece made from many threads

The Loom Never Changes

This is the most important rule in MuxCore:

If adding a new media type requires changing the core platform, the architecture is wrong.

Core — the loom — knows nothing about movies, TV shows, music, books, or any specific media type. It provides eight basic services (event routing, storage, scheduling, module discovery, etc.) and nothing more. Everything specific to a media type lives in modules and their contracts.

This means:

  • Adding support for VR180 video? Write a module, core doesn't change.
  • Adding audiobook support? Write a module, core doesn't change.
  • Adding a new streaming service? Write a module, core doesn't change.

Five Design Patterns

These five patterns are how MuxCore stays flexible without becoming complicated. They're the engineering behind "the loom never changes."

Pattern 1: Meta — The Universal Carry-All

Every task and filter in MuxCore carries a Meta field — a key-value bag that modules use for their own data. Core never looks inside it.

Why this matters: A download task can hold a torrent magnet link, an Apple Music track ID, a Spotify URI, or a YouTube video ID — all in the same Meta field. Core routes the task. The downloader module reads what it needs from Meta. Core never learns what Apple Music is.

// Core never interprets these fields — the module does
task := DownloadTask{
    Label: "Download: Blinding Lights",
    Meta: map[string]any{
        "apple_music_track_id": "1495061978",
        "stream_mode":          true,
    },
}

Pattern 2: Discovery, Not Pre-Wiring

Core provides eight essential services to every module (event bus, registry, storage, etc.). Everything else — secrets, databases, caches, metrics, tracing — modules discover at runtime by asking "who can do X?"

Module asks: "Who provides secrets?"
Registry answers: "vault-secrets can do that"
Module gets: a SecretsProvider it can use

If someone writes a new type of service tomorrow, modules discover it the same way. Core's service list never needs to grow.

Pattern 3: Role Strings, Not Enums

In most systems, the platform defines every possible module type as a fixed list. In MuxCore, modules define their own roles as simple strings:

// A module can declare any roles it wants
Roles: []string{"downloader", "streaming_source", "cache_writer"}

Core has no catalog of roles. A module can declare "vr180_processor" and other modules find it with FindByRole("vr180_processor"). The loom doesn't need to know what a VR180 processor is.

Pattern 4: Domain Contracts in Separate Repos

Everything specific to a media type lives in a contract repository — an independent, versioned package that defines interfaces:

github.com/Muxcore-Media/contracts-downloader   → Downloader, DownloadTask
github.com/Muxcore-Media/contracts-playback     → Playback, StreamSource
github.com/Muxcore-Media/contracts-media        → MediaLibrary, MediaObject
github.com/Muxcore-Media/contracts-transcoder   → TranscodeProfile, Transcoder

Core imports none of these. Modules import the ones they need. When streaming needs a new interface, contracts-playback gets updated — core stays untouched.

Pattern 5: Contract Declarations

Modules openly declare which contracts they implement:

func (m *Module) Info() ModuleInfo {
    return ModuleInfo{
        ID:   "apple-music",
        Name: "Apple Music Provider",
        Roles: []string{"playback", "media_manager"},
        Contracts: []ContractDeclaration{
            {Repo: "contracts-playback", Version: "v1.0.0", Interface: "StreamSource"},
            {Repo: "contracts-media",    Version: "v1.0.0", Interface: "MediaLibrary"},
        },
    }
}

The marketplace checks these before showing install buttons. Consumer modules check them before connecting. Core stores and returns them — nothing more.


The Media Object Pattern

One of the most powerful design decisions in MuxCore: a single media item can own multiple files.

In Sonarr, if you want 1080p and 4K versions of a movie, you need two separate instances. In MuxCore, one movie object owns files at every quality:

MediaObject "The Terminator"
├── Assets:
│   ├── 2160p remux (72 GB, MKV)
│   ├── 1080p web   (8 GB, AV1)
│   ├── 720p web    (3 GB, H.264)
│   └── 360p web    (1 GB, H.264)

The playback module asks "give me the best file this device can play," and the media library returns the matching asset. The loom just stores blobs and routes events — it never counts files or checks codecs.

How It Works in Practice

  1. Downloader fetches the 2160p remux → adds an asset to the media object
  2. Transcoder creates 1080p, 720p, and 360p versions → adds three more assets
  3. Client requests playback → media library picks the right asset for that device
  4. Core never knows any of this happened — it just stored blobs and routed events

How Modules Communicate

Modules in MuxCore never call each other directly. All communication goes through two channels:

Signals (Events)

When something changes — a download finishes, a transcode completes, a new node joins the cluster — the module publishes a signal on the event bus. Any module that cares about that signal subscribes to it:

Downloader publishes: "download.completed"
    ↓
Media Manager receives it → imports the file
Transcoder receives it → starts optimizing
Notification module receives it → sends a Discord message

The downloader doesn't know (or care) who's listening. It just sends the signal.

Direct Calls (gRPC)

For request/response patterns — "what's the status of task X?" or "give me the metadata for movie Y" — modules make direct calls through the gRPC mesh. The mesh routes calls locally when the target module is on the same machine, or over the network when it's on another node.

Module-to-module calls are protected by policy — modules declare who they are, and the mesh enforces access rules.


The Fabric at a Glance

                    ┌──────────────────────┐
                    │     Admin UI          │
                    │  (HTMX + Go templates)│
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │    API Gateway        │
                    │  (REST + OpenAPI)     │
                    └──────────┬───────────┘
                               │
          ┌────────────────────┼────────────────────┐
          │                    │                    │
   ┌──────▼──────┐    ┌───────▼───────┐    ┌───────▼──────┐
   │  Event Bus  │    │   Registry    │    │  Scheduler   │
   │ (signals)   │    │(thread dir)   │    │ (tasks)      │
   └──────┬──────┘    └───────┬───────┘    └───────┬──────┘
          │                    │                    │
          └────────────────────┼────────────────────┘
                               │
                    ┌──────────▼───────────┐
                    │   Modules (threads)   │
                    │                       │
                    │  Downloaders          │
                    │  Media Managers       │
                    │  Playback Providers   │
                    │  Transcoders          │
                    │  Storage Backends     │
                    │  Notification Senders │
                    │  ... and more         │
                    └───────────────────────┘

Next Steps

Clone this wiki locally